Skip to content

[Feat] #64 - 포트폴리오 작성용 공통 폼(Form) 컴포넌트군 구현#71

Merged
sebeeeen merged 12 commits into
devfrom
feat/form
Apr 1, 2026
Merged

[Feat] #64 - 포트폴리오 작성용 공통 폼(Form) 컴포넌트군 구현#71
sebeeeen merged 12 commits into
devfrom
feat/form

Conversation

@kimsman06
Copy link
Copy Markdown
Collaborator

@kimsman06 kimsman06 commented Mar 28, 2026

[Feat] #64 - 포트폴리오 작성용 공통 폼(Form) 컴포넌트군 구현

🔎 What is this PR?

서비스 내 다양한 입력 폼에서 공통으로 사용할 수 있는 표준화된 폼 UI 컴포넌트 세트를 구현했습니다. 특히 포트폴리오 작성
페이지의 복잡한 구조를 FSD 아키텍처 가이드에 맞춰 효율적으로 구축할 수 있도록 설계했습니다.


📝 Changes

  • 공통 폼 기초 구조 구현 (src/shared/ui/form)
    • FormField: 레이블, 입력 요소, 설명/에러 문구를 하나로 묶고 일관된 간격(Gap 8px)을 제공하는 컨테이너
    • FormLabel: 필수 입력 표시(*) 기능이 포함된 16px Medium 폰트 레이블
    • FormHelperText / FormErrorMessage: 14px 기반의 가이드 및 유효성 검사 에러 문구 표시
  • 공통 선택형 UI 구현 (src/shared/ui/checkbox)
    • 디자인 가이드 스펙 기반의 사각형 체크박스 및 브랜드 컬러(primary-800) 적용
    • peer 선택자를 활용한 네이티브 상태 연동 및 인터랙션 강화
  • 미디어 업로드 관리 UI 구현 (src/shared/ui/file-uploader)
    • ThumbnailUploader: 대표 이미지 업로드를 위한 256px 높이 영역 및 실시간 미리보기 기능
    • FileListItem: 업로드된 파일의 타입별 아이콘(이미지/문서) 매칭 및 삭제 기능을 포함한 카드형 리스트
    • FileUploader: 일반 파일 추가를 위한 드롭 영역 구현
  • 컴포넌트 통합 및 최적화
    • shared/ui/index.ts를 통한 통합 내보내기 구성
    • Storybook을 통한 포트폴리오 작성 종합 폼 시나리오(FullPortfolioForm) 구현 및 검증

📸 Screenshots

  • Storybook 내 다음 항목 확인 가능:
    • Shared/UI/Form/FullPortfolioForm: 전체 요소가 결합된 종합 예시
    • Shared/UI/Checkbox: 체크박스 리스트 및 상태별 스타일
    • Shared/UI/FileUploader: 썸네일 미리보기 및 파일 리스트 동작

📚 Background / Context

  • 포트폴리오 작성, 회원가입 등 여러 페이지에서 중복되는 폼 마크업을 최소화하고 UI 일관성을 유지하기 위함.
  • FSD 아키텍처의 shared 레이어 원칙에 따라 순수 UI와 제어 가능한 Props 구조로 설계하여 비즈니스 로직과의 분리를 명확히
    함.

✔ Checklist

  • 코드는 로컬에서 정상적으로 빌드됩니다 (pnpm build)
  • ESLint / Prettier 통과 (pnpm lint)
  • 네이밍/레이어 컨벤션 준수 (camelCase/PascalCase, is·has 불린 접두사, alias 계층 규칙)
  • 관련 문서/주석 반영 (필요 시)
  • 주요 로직에 테스트 또는 검증 완료 (Storybook 종합 폼 시나리오)

🙏 Request

Summary by CodeRabbit

  • New Features

    • Added a Checkbox component for user selections.
    • Introduced File Upload UI with thumbnail preview, drag-and-drop, multi-file support, removable file list, and composite uploader layouts.
    • Added form building blocks: Field container, Label, Helper Text, and Error Message.
    • Added new icons for file, upload, and image.
  • Documentation

    • Added Storybook examples showcasing checkbox, file uploader, and form usage.

@kimsman06 kimsman06 self-assigned this Mar 28, 2026
@kimsman06 kimsman06 linked an issue Mar 28, 2026 that may be closed by this pull request
7 tasks
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 28, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 702a6045-31bf-49a7-918d-6c5ebc4cae55

📥 Commits

Reviewing files that changed from the base of the PR and between ba03d47 and f02cf14.

📒 Files selected for processing (2)
  • src/shared/ui/icons/index.tsx
  • src/shared/ui/index.ts

📝 Walkthrough

Walkthrough

Adds new shared UI primitives: form components, a Checkbox, file uploader components (including thumbnail preview and file list), Storybook stories for these components, three SVG icons, and updates the shared UI barrel exports.

Changes

Cohort / File(s) Summary
Checkbox
src/shared/ui/checkbox/checkbox.tsx, src/shared/ui/checkbox/checkbox.stories.tsx, src/shared/ui/checkbox/index.ts
New Checkbox component (forwardRef) with label prop, disabled/checked styling and animated check icon; Storybook stories including an InteractiveCheckbox wrapper; barrel export added.
Form primitives
src/shared/ui/form/form.tsx, src/shared/ui/form/form.stories.tsx, src/shared/ui/form/index.ts
New FormField, FormLabel, FormHelperText, FormErrorMessage components and Storybook stories demonstrating full form, default, and error states; barrel export added.
File uploader
src/shared/ui/file-uploader/file-uploader.tsx, src/shared/ui/file-uploader/file-uploader.stories.tsx, src/shared/ui/file-uploader/index.ts
Adds FileUploader (dropzone/multi-file), ThumbnailUploader (single-image preview + remove), and FileListItem (file row with icon/delete). Stories: Thumbnail, FileList, CombinedUploader; barrel export added.
Icons
src/shared/ui/icons/index.tsx
Adds FileTextIcon, UploadIcon, and ImageIcon SVG components (accept IconProps, default size 20). Note: a duplicate FileTextIcon export was introduced in the same module.
Shared UI exports
src/shared/ui/index.ts
Updated barrel to re-export ./form, ./checkbox, and ./file-uploader, expanding public exports.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant Uploader as FileUploader component
  participant Input as Hidden <input type="file">
  participant Consumer as onFileSelect handler
  User->>Uploader: click / drop files
  Uploader->>Input: open file picker / receive drop
  Input-->>Uploader: FileList selected
  Uploader->>Uploader: convert FileList -> File[]
  Uploader->>Consumer: onFileSelect(files)
  Consumer-->>Uploader: (handles files)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • jaeu5325
  • 1-J-1

Poem

🐰
I hopped through code with tiny paws,
Checked boxes, stacked the file upload laws,
Labels snug and previews bright,
Icons gleam in morning light,
A little hop — the UI’s just right! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main objective: implementing a standardized form component set for portfolio creation, which is the primary focus of all changes in the PR.
Description check ✅ Passed The description follows the template structure with all required sections (What, Changes, Screenshots, Background, Checklist) properly filled out and completed with detailed information about implementation and validation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/form

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (3)
src/shared/ui/checkbox/checkbox.stories.tsx (1)

19-20: Story control state can desync from args.checked.

At Line [19], local state is initialized once. If controls change checked, the rendered checkbox may not follow.

🧪 Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
...
 const InteractiveCheckbox = (args: CheckboxProps) => {
   const [checked, setChecked] = useState(args.checked || false);
+  useEffect(() => {
+    setChecked(Boolean(args.checked));
+  }, [args.checked]);
   return <Checkbox {...args} checked={checked} onChange={(e) => setChecked(e.target.checked)} />;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/checkbox/checkbox.stories.tsx` around lines 19 - 20, The story
initializes local state with useState(args.checked || false) which won't update
when controls change args.checked; in the story component (where useState,
checked, setChecked and <Checkbox {...args} checked={checked} onChange=...} are
used) replace or augment the state with a synchronization step: add an effect
that watches args.checked and calls setChecked(args.checked) (or use
args.checked directly to render the Checkbox as a controlled prop) so the
rendered Checkbox follows storybook control changes.
src/shared/ui/file-uploader/file-uploader.stories.tsx (1)

47-66: Combined story currently can’t validate the core interaction flow.

At Line 54, Line 62, and Line 65, handlers are no-ops, so upload/remove paths aren’t actually exercised in this “combined” scenario. Consider wiring minimal local state here too (like in Thumbnail) so this story verifies behavior, not just layout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/file-uploader/file-uploader.stories.tsx` around lines 47 - 66,
The CombinedUploader story uses no-op handlers so upload/remove flows aren't
exercised; change the story to hold minimal local React state (e.g., thumbnail
state and an array of files) and implement the handlers passed to
ThumbnailUploader (onUpload, onRemove) and FileUploader (onFileSelect) and
FileListItem (onRemove) to update that state (add/remove items) so the combined
story actually demonstrates interaction and state changes for the components
ThumbnailUploader, FileUploader, and FileListItem.
src/shared/ui/form/form.stories.tsx (1)

100-100: FileUploader is rendered but add-file behavior is not demonstrated.

At Line 100, onFileSelect={() => {}} makes the integrated form story partially non-functional. Hooking this to setFiles would make the Storybook scenario much more useful for regression checks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/form/form.stories.tsx` at line 100, The FileUploader in the
story is rendered with a no-op onFileSelect which prevents the story from
demonstrating add-file behavior; update the story to connect FileUploader's
onFileSelect to the story's state updater (e.g., call setFiles or the story args
setter) so selected files update the component state—locate the FileUploader
usage in the form story (FileUploader onFileSelect prop) and replace the empty
handler with a call that forwards the selected files to setFiles (or the
appropriate state/args setter) to enable interactive file-add behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/shared/ui/checkbox/checkbox.tsx`:
- Around line 13-17: The visible checkbox lacks a keyboard focus indicator:
update the inputHiddenClasses constant to include the "peer" class so the hidden
input can drive peer styles, and update boxBaseClasses to add appropriate
"peer-focus-visible:..." utilities (e.g. peer-focus-visible:ring-2
peer-focus-visible:ring-offset-1 peer-focus-visible:ring-primary or your design
tokens) so the visible box shows a focus ring when keyboard-focused; modify the
constants inputHiddenClasses and boxBaseClasses accordingly to enable a clear
focus-visible state.
- Around line 50-64: The Checkbox component currently only derives visual state
from the checked prop, breaking uncontrolled usage; update Checkbox (the
React.forwardRef function) to support controlled and uncontrolled modes by
deriving an internal state (e.g., localChecked) initialized from
props.defaultChecked when props.checked is undefined, use a computed value
(controlledChecked = props.checked ?? localChecked) for getBoxClasses and
CheckIcon, and wire the input's onChange to update localChecked and call any
provided props.onChange so both native input state and visual state stay in
sync; ensure you only pass a checked attribute to the <input> when in controlled
mode and otherwise let the input manage its own defaultChecked.

In `@src/shared/ui/file-uploader/file-uploader.tsx`:
- Around line 101-103: The UI advertises drag-and-drop but the FileUploader
component lacks any drag handlers; add drag support by wiring
onDragOver/onDragEnter/onDragLeave/onDrop on the upload wrapper inside
file-uploader.tsx, preventDefault in those handlers, extract files from
event.dataTransfer.files in onDrop and pass them to the same file-processing
routine used by the input change handler (e.g., the existing handleFileSelect or
onFilesSelected function), and optionally track an isDragging state to toggle a
visual class while dragging; alternatively, if you prefer click-only, change the
copy in the <p> text to remove "드래그하거나" so it only mentions clicking.
- Around line 57-60: The file input change handlers (handleFileChange) in
ThumbnailUploader and FileUploader should reset the input value after processing
so selecting the same file again will fire change; update both handleFileChange
functions to set e.currentTarget.value = "" after calling onUpload(file) (or
after the early-return logic) to ensure the input is cleared for subsequent
identical-file uploads.
- Around line 85-92: The upload trigger uses a non-semantic div with onClick
(see getUploaderClasses, handleClick, fileInputRef, handleFileChange) and must
be keyboard-accessible: add role="button", tabIndex={0}, and a descriptive
aria-label to the div, and implement an onKeyDown handler that invokes the same
behavior as handleClick when Enter or Space is pressed; apply the identical
changes to the other upload-trigger div used elsewhere in this component so both
triggers support keyboard and assistive technology.

In `@src/shared/ui/form/form.stories.tsx`:
- Around line 92-97: The FileListItem onRemove handler closes over the current
files array causing potential stale state; change the setFiles call inside the
FileListItem prop to the functional updater form so updates use the latest state
(replace setFiles(files.filter(f => f.id !== id)) with setFiles(prev =>
prev.filter(f => f.id !== id))). Update the code where FileListItem is rendered
(the files map and onRemove prop) to use this functional update.

In `@src/shared/ui/form/form.tsx`:
- Around line 9-13: The FormLabel currently always renders a <label> even when
htmlFor is undefined; update the component that uses the FormLabelProps (the
FormLabel render function/component) to render a <span> instead of a <label>
when props.htmlFor is falsy, preserving all children, required indicator and
className handling; keep behavior unchanged when htmlFor is provided (render a
<label htmlFor={htmlFor}>), and ensure typings (FormLabelProps) remain the same
so this change is non-breaking.

---

Nitpick comments:
In `@src/shared/ui/checkbox/checkbox.stories.tsx`:
- Around line 19-20: The story initializes local state with
useState(args.checked || false) which won't update when controls change
args.checked; in the story component (where useState, checked, setChecked and
<Checkbox {...args} checked={checked} onChange=...} are used) replace or augment
the state with a synchronization step: add an effect that watches args.checked
and calls setChecked(args.checked) (or use args.checked directly to render the
Checkbox as a controlled prop) so the rendered Checkbox follows storybook
control changes.

In `@src/shared/ui/file-uploader/file-uploader.stories.tsx`:
- Around line 47-66: The CombinedUploader story uses no-op handlers so
upload/remove flows aren't exercised; change the story to hold minimal local
React state (e.g., thumbnail state and an array of files) and implement the
handlers passed to ThumbnailUploader (onUpload, onRemove) and FileUploader
(onFileSelect) and FileListItem (onRemove) to update that state (add/remove
items) so the combined story actually demonstrates interaction and state changes
for the components ThumbnailUploader, FileUploader, and FileListItem.

In `@src/shared/ui/form/form.stories.tsx`:
- Line 100: The FileUploader in the story is rendered with a no-op onFileSelect
which prevents the story from demonstrating add-file behavior; update the story
to connect FileUploader's onFileSelect to the story's state updater (e.g., call
setFiles or the story args setter) so selected files update the component
state—locate the FileUploader usage in the form story (FileUploader onFileSelect
prop) and replace the empty handler with a call that forwards the selected files
to setFiles (or the appropriate state/args setter) to enable interactive
file-add behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 817a42ff-1393-41bd-a586-3e106d9717d4

📥 Commits

Reviewing files that changed from the base of the PR and between f9195e7 and ea784e3.

📒 Files selected for processing (11)
  • src/shared/ui/checkbox/checkbox.stories.tsx
  • src/shared/ui/checkbox/checkbox.tsx
  • src/shared/ui/checkbox/index.ts
  • src/shared/ui/file-uploader/file-uploader.stories.tsx
  • src/shared/ui/file-uploader/file-uploader.tsx
  • src/shared/ui/file-uploader/index.ts
  • src/shared/ui/form/form.stories.tsx
  • src/shared/ui/form/form.tsx
  • src/shared/ui/form/index.ts
  • src/shared/ui/icons/index.tsx
  • src/shared/ui/index.ts

Comment thread src/shared/ui/checkbox/checkbox.tsx Outdated
Comment on lines +50 to +64
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
({ label, checked, disabled, className = "", ...props }, ref) => {
return (
<label className={[containerBaseClasses, className].join(" ")}>
<input
type="checkbox"
ref={ref}
checked={checked}
disabled={disabled}
className={inputHiddenClasses}
{...props}
/>
<div className={getBoxClasses(checked, disabled)}>
<CheckIcon size={14} className={checked ? "text-white" : "text-transparent"} />
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/shared/ui/checkbox/checkbox.tsx

Repository: ajou-industry-matching/aim-frontend

Length of output: 2768


🏁 Script executed:

rg -A 10 "type CheckboxProps" src/shared/ui/checkbox/

Repository: ajou-industry-matching/aim-frontend

Length of output: 1524


Support both controlled and uncontrolled modes for the Checkbox component.

Visual state breaks when using uncontrolled mode (defaultChecked without checked). The component accepts defaultChecked via React.InputHTMLAttributes, but the visual state (box and icon) only depends on the checked prop. When checked is undefined, the UI remains visually unchecked while the native input tracks its own state independently.

Implement the controlled/uncontrolled pattern to handle both cases:

🔧 Proposed fix
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
-  ({ label, checked, disabled, className = "", ...props }, ref) => {
+  ({ label, checked, defaultChecked, disabled, className = "", onChange, ...props }, ref) => {
+    const isControlled = checked !== undefined;
+    const [internalChecked, setInternalChecked] = React.useState(Boolean(defaultChecked));
+    const isChecked = isControlled ? checked : internalChecked;
+
+    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (!isControlled) setInternalChecked(e.target.checked);
+      onChange?.(e);
+    };
+
     return (
       <label className={[containerBaseClasses, className].join(" ")}>
         <input
           type="checkbox"
           ref={ref}
-          checked={checked}
+          checked={isChecked}
           disabled={disabled}
           className={inputHiddenClasses}
+          onChange={handleChange}
           {...props}
         />
-        <div className={getBoxClasses(checked, disabled)}>
-          <CheckIcon size={14} className={checked ? "text-white" : "text-transparent"} />
+        <div className={getBoxClasses(isChecked, disabled)}>
+          <CheckIcon size={14} className={isChecked ? "text-white" : "text-transparent"} />
         </div>
         {label && <span className={getLabelClasses(disabled)}>{label}</span>}
       </label>
     );
   },
 );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/shared/ui/checkbox/checkbox.tsx` around lines 50 - 64, The Checkbox
component currently only derives visual state from the checked prop, breaking
uncontrolled usage; update Checkbox (the React.forwardRef function) to support
controlled and uncontrolled modes by deriving an internal state (e.g.,
localChecked) initialized from props.defaultChecked when props.checked is
undefined, use a computed value (controlledChecked = props.checked ??
localChecked) for getBoxClasses and CheckIcon, and wire the input's onChange to
update localChecked and call any provided props.onChange so both native input
state and visual state stay in sync; ensure you only pass a checked attribute to
the <input> when in controlled mode and otherwise let the input manage its own
defaultChecked.

Comment thread src/shared/ui/file-uploader/file-uploader.tsx
Comment thread src/shared/ui/file-uploader/file-uploader.tsx Outdated
Comment thread src/shared/ui/file-uploader/file-uploader.tsx
Comment thread src/shared/ui/form/form.stories.tsx Outdated
Comment thread src/shared/ui/form/form.tsx
@kimsman06 kimsman06 added the enhancement New feature or request label Mar 28, 2026
Comment on lines +4 to +12
import { Input, Textarea } from "@/shared/ui/inputBox/inputBox";
import { Checkbox } from "@/shared/ui/checkbox/checkbox";
import {
ThumbnailUploader,
FileListItem,
FileUploader,
type FileItem,
} from "@/shared/ui/file-uploader/file-uploader";
import { Button } from "@/shared/ui/button/button";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

내부 파일을 직접 참조하면 내부 구현이 변경될 때 import 경로가 깨질 수 있어 아래와 같이 index.ts(public API)를 통해 import하는 방식을 제안드립니다~!

import { Input, Textarea } from "@/shared/ui/inputBox";
import { Checkbox } from "@/shared/ui/checkbox";
import { ThumbnailUploader, ... } from "@/shared/ui/file-uploader";
import { Button } from "@/shared/ui/button";

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 공통 폼(Form) 컴포넌트군 구현

2 participants