임동욱 임동욱 08-20
fix: 만화 컷 업로드를 시안(5449:10857)의 컷 카드로 맞춘다
컷당 구성이 시안과 달랐다. 시안은 테두리 컨테이너 안 회색 판(#E6E8EA) 위에 흰 컷 카드
(radius 12·neutral/lv1 그림자)가 4열로 깔리고, 카드마다 **점선 상자 자체가 파일선택 버튼**
(CameraPlus 중앙, 높이 172)이며 아래에 「N컷 · 초기화 · 삭제」가 붙는다. 「컷 추가」는 컨테이너
흰 푸터에 있다. 기존 구현은 테두리 박스 안에 작은 132×74 썸네일 + 파일선택 버튼이었고
초기화가 없었다.

FoxFileUpload에 `thumbnailPicker` 변형을 추가했다 — 썸네일 상자만 그리고 상자를 파일선택
버튼으로 쓴다(파일선택 버튼·설명·삭제 오버레이 없음). 시안의 이 패턴을 담을 형태가 image
모드에 없었다. 비우기·삭제는 호출부가 바깥에 만든다는 계약을 주석으로 못박았다.

초기화는 그 컷의 React key를 갈아 끼워 새로 마운트한다 — 비제어 FoxFileUpload가 들고 있는
파일과 네이티브 입력을 밖에서 비울 방법이 그것뿐이다. 초기화하면 기존 첨부 유지용
`cutFileId`도 함께 비워져 저장 시 그 컷이 빠진다.

확인(임시 페이지): 상자 클릭 대상 aria-label 「1컷 이미지 선택」, 파일 주입 → 미리보기 표시,
초기화 → 미리보기·네이티브 입력 모두 비워짐, 컷 추가 → 5컷에서 삭제 활성, 삭제 → 4컷에서
비활성. 컨테이너 테두리 #B1B8BE·radius 12·판 #E6E8EA·카드 그림자·점선 #CDD1D5·높이 172가
계산값으로 시안과 일치.

Co-Authored-By: Claude Fable 5 
@4561a14b4e6b1aed82220d62b6406c7f6774ef4f
@fox/core/components/fox-file-upload/fox-file-upload.tsx
--- @fox/core/components/fox-file-upload/fox-file-upload.tsx
+++ @fox/core/components/fox-file-upload/fox-file-upload.tsx
@@ -73,6 +73,12 @@
   /** `image` 모드 썸네일 크기. 시안은 213×120px(16:9)이고 토큰에 없어 여기서 받는다. */
   thumbnailWidth?: string | number;
   thumbnailHeight?: string | number;
+  /**
+   * `image` 모드에서 썸네일 상자만 그리고 **상자 자체를 파일선택 버튼**으로 쓴다 — 파일선택
+   * 버튼·설명·삭제 오버레이가 없다(시안 tbl-form의 컷 업로드). 채워진 뒤에도 상자를 누르면
+   * 다시 고른다. 비우기·삭제가 필요하면 호출부가 바깥에 만든다.
+   */
+  thumbnailPicker?: boolean;
 
   disabled?: boolean;
   /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
@@ -179,6 +185,7 @@
   inputWidth = 320,
   thumbnailWidth = 213,
   thumbnailHeight = 120,
+  thumbnailPicker = false,
   disabled = false,
   hidden = false,
   className,
@@ -373,7 +380,34 @@
         </div>
       )}
 
-      {mode === "image" && (
+      {mode === "image" && thumbnailPicker && (
+        <button
+          type="button"
+          className={cx(
+            "fox-file-upload__thumb",
+            "fox-file-upload__thumb--picker",
+            preview?.previewUrl && "fox-file-upload__thumb--filled"
+          )}
+          style={thumbStyle}
+          aria-label={selectLabel}
+          disabled={disabled}
+          onClick={openPicker}
+        >
+          {preview?.previewUrl ? (
+            <>
+              {/* eslint-disable-next-line @next/next/no-img-element -- next/image를 쓰면 이
+                  폴더가 Next 전용이 된다. */}
+              <img className="fox-file-upload__thumb-image" src={preview.previewUrl} alt="" />
+            </>
+          ) : (
+            <span className="fox-file-upload__thumb-icon" aria-hidden="true">
+              <CameraPlusIcon />
+            </span>
+          )}
+        </button>
+      )}
+
+      {mode === "image" && !thumbnailPicker && (
         <div className="fox-file-upload__image">
           <div
             className={cx(
@fox/styles/_fox-file-upload.scss
--- @fox/styles/_fox-file-upload.scss
+++ @fox/styles/_fox-file-upload.scss
@@ -116,6 +116,20 @@
     color: fox.color(icon-neutral-fill);
   }
 
+  // 상자 자체가 파일선택 버튼인 변형(`thumbnailPicker`) — 버튼 기본 모양만 걷어내고
+  // 초점 고리는 브라우저 것을 그대로 둔다.
+  &__thumb--picker {
+    padding: 0;
+    margin: 0;
+    appearance: none;
+    font: inherit;
+    cursor: pointer;
+
+    &:disabled {
+      cursor: not-allowed;
+    }
+  }
+
   // 그림이 들어오면 점선이 실선이 되고 바탕은 그림이 덮는다.
   &__thumb--filled {
     border-style: solid;
app/(protected)/(basic)/contents/_components/content-cuts-field.tsx
--- app/(protected)/(basic)/contents/_components/content-cuts-field.tsx
+++ app/(protected)/(basic)/contents/_components/content-cuts-field.tsx
@@ -19,8 +19,13 @@
 type Cut = { key: number; url: string | null };
 
 /**
- * 만화 이미지 — 기본 4컷을 깔고 「컷 추가」로 계속 늘린다(기획 "4컷 이상 구성도 가능").
- * 각 컷은 개별 이미지로 올린다.
+ * 만화 이미지 — 시안 tbl-form(5449:10857)의 컷 업로드.
+ *
+ * 회색 판 위에 흰 컷 카드 4개가 깔리고 「컷 추가」로 계속 늘린다(기획 "4컷 이상 구성도 가능").
+ * 컷마다 점선 상자 자체가 파일선택 버튼이고, 아래에 「N컷 · 초기화 · 삭제」가 붙는다.
+ *
+ * 초기화는 그 컷의 선택만 비운다 — React key를 갈아 끼워 새로 마운트한다. 비제어
+ * `FoxFileUpload`가 들고 있는 파일과 네이티브 입력을 밖에서 비울 방법이 그것뿐이다.
  */
 export function ContentCutsField({
   defaultCutUrls = [],
@@ -41,6 +46,15 @@
     setNextKey((current) => current + 1);
   }
 
+  function resetCut(key: number) {
+    setCuts((current) =>
+      current.map((cut) =>
+        cut.key === key ? { key: nextKey, url: null } : cut
+      )
+    );
+    setNextKey((current) => current + 1);
+  }
+
   function removeCut(key: number) {
     setCuts((current) =>
       current.length <= MIN_COMIC_CUTS
@@ -51,48 +65,61 @@
 
   return (
     <>
-      <div className={styles.cuts}>
-        {cuts.map((cut, index) => (
-          <div key={cut.key} className={styles.cut}>
-            <FoxFileUpload
-              mode="image"
-              name="cutFile"
-              accept={fileModuleAccept(FILE_MODULE.toonContent)}
-              selectLabel="파일선택"
-              thumbnailWidth={132}
-              thumbnailHeight={74}
-              defaultFiles={
-                cut.url
-                  ? [{ id: String(cut.key), name: `${index + 1}컷`, previewUrl: cut.url }]
-                  : undefined
-              }
-            />
-            <input
-              type="hidden"
-              name="cutFileId"
-              defaultValue={cut.url ? (retainedFileId ?? '') : ''}
-            />
-            <div className={styles.cutFooter}>
-              <span className={styles.cutLabel}>{index + 1}컷</span>
-              <FoxTextButton
-                size="sm"
-                leadingIcon={<i className="fox-ico fox-ico-Trash" aria-hidden="true" />}
-                label="삭제"
-                disabled={cuts.length <= MIN_COMIC_CUTS}
-                onAction={() => removeCut(cut.key)}
+      <div className={styles.cutBox}>
+        <div className={styles.cutBody}>
+          {cuts.map((cut, index) => (
+            <div key={cut.key} className={styles.cutCard}>
+              <FoxFileUpload
+                mode="image"
+                name="cutFile"
+                accept={fileModuleAccept(FILE_MODULE.toonContent)}
+                thumbnailPicker
+                selectLabel={`${index + 1}컷 이미지 선택`}
+                thumbnailWidth="100%"
+                thumbnailHeight={172}
+                defaultFiles={
+                  cut.url
+                    ? [{ id: String(cut.key), name: `${index + 1}컷`, previewUrl: cut.url }]
+                    : undefined
+                }
               />
+              <input
+                type="hidden"
+                name="cutFileId"
+                defaultValue={cut.url ? (retainedFileId ?? '') : ''}
+              />
+              <div className={styles.cutFooter}>
+                <span className={styles.cutLabel}>{index + 1}컷</span>
+                <span className={styles.cutActions}>
+                  <FoxTextButton
+                    size="md"
+                    type="secondary"
+                    leadingIcon={<i className="fox-ico fox-ico-ArrowClockwise" aria-hidden="true" />}
+                    label="초기화"
+                    onAction={() => resetCut(cut.key)}
+                  />
+                  <FoxTextButton
+                    size="md"
+                    type="secondary"
+                    leadingIcon={<i className="fox-ico fox-ico-Trash" aria-hidden="true" />}
+                    label="삭제"
+                    disabled={cuts.length <= MIN_COMIC_CUTS}
+                    onAction={() => removeCut(cut.key)}
+                  />
+                </span>
+              </div>
             </div>
-          </div>
-        ))}
-      </div>
+          ))}
+        </div>
 
-      <div>
-        <FoxTextButton
-          size="md"
-          leadingIcon={<i className="fox-ico fox-ico-Plus" aria-hidden="true" />}
-          label="컷 추가"
-          onAction={addCut}
-        />
+        <div className={styles.cutBoxFooter}>
+          <FoxTextButton
+            size="md"
+            leadingIcon={<i className="fox-ico fox-ico-Plus" aria-hidden="true" />}
+            label="컷 추가"
+            onAction={addCut}
+          />
+        </div>
       </div>
 
       {error && <FoxHelperText type="danger" message={error} />}
app/(protected)/(basic)/contents/_components/content-form.module.scss
--- app/(protected)/(basic)/contents/_components/content-form.module.scss
+++ app/(protected)/(basic)/contents/_components/content-form.module.scss
@@ -59,31 +59,58 @@
   min-inline-size: 0;
 }
 
-.cuts {
+// 시안 tbl-form(5449:10857)의 슬롯 — 테두리 컨테이너 안에 회색 판(컷 카드들)과 흰 푸터(컷 추가).
+.cutBox {
+  inline-size: 100%;
+  overflow: hidden;
+  border: fox.border(1) solid fox.color(border-neutral-default);
+  border-radius: fox.radius(4);
+  background: fox.color(surface-neutral-default);
+}
+
+.cutBody {
   display: grid;
   grid-template-columns: repeat(4, minmax(0, 1fr));
   gap: fox.gap(4);
+  padding-block: fox.padding(5);
+  padding-inline: fox.padding(6);
+  background: fox.color(surface-neutral-gray-strong);
 }
 
-.cut {
+.cutCard {
   display: flex;
   flex-direction: column;
-  gap: fox.gap(2);
+  gap: fox.gap(3);
   padding: fox.padding(3);
-  border: fox.border(1) solid fox.color(border-neutral-subtler);
-  border-radius: fox.radius(2);
+  border-radius: fox.radius(4);
+  background: fox.color(surface-neutral-default);
+  box-shadow:
+    fox.shadow(location1) fox.shadow(location1) fox.shadow(blur1) 0
+      fox.primitive(alpha-black10);
 }
 
 .cutFooter {
   display: flex;
   align-items: center;
   justify-content: space-between;
-  gap: fox.gap(2);
+  gap: fox.gap(3);
 }
 
 .cutLabel {
   font-size: fox.font-size(label-sm);
-  color: fox.color(font-neutral-subtle);
+  color: fox.color(font-neutral-default);
+}
+
+.cutActions {
+  display: flex;
+  align-items: center;
+  gap: fox.gap(3);
+}
+
+.cutBoxFooter {
+  display: flex;
+  padding-block: fox.padding(4);
+  padding-inline: fox.padding(6);
 }
 
 .actions {
Add a comment
List