--- @fox/core/components/fox-file-upload/fox-file-upload.tsx
+++ @fox/core/components/fox-file-upload/fox-file-upload.tsx
... | ... | @@ -73,6 +73,12 @@ |
| 73 | 73 |
/** `image` 모드 썸네일 크기. 시안은 213×120px(16:9)이고 토큰에 없어 여기서 받는다. */ |
| 74 | 74 |
thumbnailWidth?: string | number; |
| 75 | 75 |
thumbnailHeight?: string | number; |
| 76 |
+ /** |
|
| 77 |
+ * `image` 모드에서 썸네일 상자만 그리고 **상자 자체를 파일선택 버튼**으로 쓴다 — 파일선택 |
|
| 78 |
+ * 버튼·설명·삭제 오버레이가 없다(시안 tbl-form의 컷 업로드). 채워진 뒤에도 상자를 누르면 |
|
| 79 |
+ * 다시 고른다. 비우기·삭제가 필요하면 호출부가 바깥에 만든다. |
|
| 80 |
+ */ |
|
| 81 |
+ thumbnailPicker?: boolean; |
|
| 76 | 82 |
|
| 77 | 83 |
disabled?: boolean; |
| 78 | 84 |
/** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ |
... | ... | @@ -179,6 +185,7 @@ |
| 179 | 185 |
inputWidth = 320, |
| 180 | 186 |
thumbnailWidth = 213, |
| 181 | 187 |
thumbnailHeight = 120, |
| 188 |
+ thumbnailPicker = false, |
|
| 182 | 189 |
disabled = false, |
| 183 | 190 |
hidden = false, |
| 184 | 191 |
className, |
... | ... | @@ -373,7 +380,34 @@ |
| 373 | 380 |
</div> |
| 374 | 381 |
)} |
| 375 | 382 |
|
| 376 |
- {mode === "image" && (
|
|
| 383 |
+ {mode === "image" && thumbnailPicker && (
|
|
| 384 |
+ <button |
|
| 385 |
+ type="button" |
|
| 386 |
+ className={cx(
|
|
| 387 |
+ "fox-file-upload__thumb", |
|
| 388 |
+ "fox-file-upload__thumb--picker", |
|
| 389 |
+ preview?.previewUrl && "fox-file-upload__thumb--filled" |
|
| 390 |
+ )} |
|
| 391 |
+ style={thumbStyle}
|
|
| 392 |
+ aria-label={selectLabel}
|
|
| 393 |
+ disabled={disabled}
|
|
| 394 |
+ onClick={openPicker}
|
|
| 395 |
+ > |
|
| 396 |
+ {preview?.previewUrl ? (
|
|
| 397 |
+ <> |
|
| 398 |
+ {/* eslint-disable-next-line @next/next/no-img-element -- next/image를 쓰면 이
|
|
| 399 |
+ 폴더가 Next 전용이 된다. */} |
|
| 400 |
+ <img className="fox-file-upload__thumb-image" src={preview.previewUrl} alt="" />
|
|
| 401 |
+ </> |
|
| 402 |
+ ) : ( |
|
| 403 |
+ <span className="fox-file-upload__thumb-icon" aria-hidden="true"> |
|
| 404 |
+ <CameraPlusIcon /> |
|
| 405 |
+ </span> |
|
| 406 |
+ )} |
|
| 407 |
+ </button> |
|
| 408 |
+ )} |
|
| 409 |
+ |
|
| 410 |
+ {mode === "image" && !thumbnailPicker && (
|
|
| 377 | 411 |
<div className="fox-file-upload__image"> |
| 378 | 412 |
<div |
| 379 | 413 |
className={cx(
|
--- @fox/styles/_fox-file-upload.scss
+++ @fox/styles/_fox-file-upload.scss
... | ... | @@ -116,6 +116,20 @@ |
| 116 | 116 |
color: fox.color(icon-neutral-fill); |
| 117 | 117 |
} |
| 118 | 118 |
|
| 119 |
+ // 상자 자체가 파일선택 버튼인 변형(`thumbnailPicker`) — 버튼 기본 모양만 걷어내고 |
|
| 120 |
+ // 초점 고리는 브라우저 것을 그대로 둔다. |
|
| 121 |
+ &__thumb--picker {
|
|
| 122 |
+ padding: 0; |
|
| 123 |
+ margin: 0; |
|
| 124 |
+ appearance: none; |
|
| 125 |
+ font: inherit; |
|
| 126 |
+ cursor: pointer; |
|
| 127 |
+ |
|
| 128 |
+ &:disabled {
|
|
| 129 |
+ cursor: not-allowed; |
|
| 130 |
+ } |
|
| 131 |
+ } |
|
| 132 |
+ |
|
| 119 | 133 |
// 그림이 들어오면 점선이 실선이 되고 바탕은 그림이 덮는다. |
| 120 | 134 |
&__thumb--filled {
|
| 121 | 135 |
border-style: solid; |
--- app/(protected)/(basic)/contents/_components/content-cuts-field.tsx
+++ app/(protected)/(basic)/contents/_components/content-cuts-field.tsx
... | ... | @@ -19,8 +19,13 @@ |
| 19 | 19 |
type Cut = { key: number; url: string | null };
|
| 20 | 20 |
|
| 21 | 21 |
/** |
| 22 |
- * 만화 이미지 — 기본 4컷을 깔고 「컷 추가」로 계속 늘린다(기획 "4컷 이상 구성도 가능"). |
|
| 23 |
- * 각 컷은 개별 이미지로 올린다. |
|
| 22 |
+ * 만화 이미지 — 시안 tbl-form(5449:10857)의 컷 업로드. |
|
| 23 |
+ * |
|
| 24 |
+ * 회색 판 위에 흰 컷 카드 4개가 깔리고 「컷 추가」로 계속 늘린다(기획 "4컷 이상 구성도 가능"). |
|
| 25 |
+ * 컷마다 점선 상자 자체가 파일선택 버튼이고, 아래에 「N컷 · 초기화 · 삭제」가 붙는다. |
|
| 26 |
+ * |
|
| 27 |
+ * 초기화는 그 컷의 선택만 비운다 — React key를 갈아 끼워 새로 마운트한다. 비제어 |
|
| 28 |
+ * `FoxFileUpload`가 들고 있는 파일과 네이티브 입력을 밖에서 비울 방법이 그것뿐이다. |
|
| 24 | 29 |
*/ |
| 25 | 30 |
export function ContentCutsField({
|
| 26 | 31 |
defaultCutUrls = [], |
... | ... | @@ -41,6 +46,15 @@ |
| 41 | 46 |
setNextKey((current) => current + 1); |
| 42 | 47 |
} |
| 43 | 48 |
|
| 49 |
+ function resetCut(key: number) {
|
|
| 50 |
+ setCuts((current) => |
|
| 51 |
+ current.map((cut) => |
|
| 52 |
+ cut.key === key ? { key: nextKey, url: null } : cut
|
|
| 53 |
+ ) |
|
| 54 |
+ ); |
|
| 55 |
+ setNextKey((current) => current + 1); |
|
| 56 |
+ } |
|
| 57 |
+ |
|
| 44 | 58 |
function removeCut(key: number) {
|
| 45 | 59 |
setCuts((current) => |
| 46 | 60 |
current.length <= MIN_COMIC_CUTS |
... | ... | @@ -51,48 +65,61 @@ |
| 51 | 65 |
|
| 52 | 66 |
return ( |
| 53 | 67 |
<> |
| 54 |
- <div className={styles.cuts}>
|
|
| 55 |
- {cuts.map((cut, index) => (
|
|
| 56 |
- <div key={cut.key} className={styles.cut}>
|
|
| 57 |
- <FoxFileUpload |
|
| 58 |
- mode="image" |
|
| 59 |
- name="cutFile" |
|
| 60 |
- accept={fileModuleAccept(FILE_MODULE.toonContent)}
|
|
| 61 |
- selectLabel="파일선택" |
|
| 62 |
- thumbnailWidth={132}
|
|
| 63 |
- thumbnailHeight={74}
|
|
| 64 |
- defaultFiles={
|
|
| 65 |
- cut.url |
|
| 66 |
- ? [{ id: String(cut.key), name: `${index + 1}컷`, previewUrl: cut.url }]
|
|
| 67 |
- : undefined |
|
| 68 |
- } |
|
| 69 |
- /> |
|
| 70 |
- <input |
|
| 71 |
- type="hidden" |
|
| 72 |
- name="cutFileId" |
|
| 73 |
- defaultValue={cut.url ? (retainedFileId ?? '') : ''}
|
|
| 74 |
- /> |
|
| 75 |
- <div className={styles.cutFooter}>
|
|
| 76 |
- <span className={styles.cutLabel}>{index + 1}컷</span>
|
|
| 77 |
- <FoxTextButton |
|
| 78 |
- size="sm" |
|
| 79 |
- leadingIcon={<i className="fox-ico fox-ico-Trash" aria-hidden="true" />}
|
|
| 80 |
- label="삭제" |
|
| 81 |
- disabled={cuts.length <= MIN_COMIC_CUTS}
|
|
| 82 |
- onAction={() => removeCut(cut.key)}
|
|
| 68 |
+ <div className={styles.cutBox}>
|
|
| 69 |
+ <div className={styles.cutBody}>
|
|
| 70 |
+ {cuts.map((cut, index) => (
|
|
| 71 |
+ <div key={cut.key} className={styles.cutCard}>
|
|
| 72 |
+ <FoxFileUpload |
|
| 73 |
+ mode="image" |
|
| 74 |
+ name="cutFile" |
|
| 75 |
+ accept={fileModuleAccept(FILE_MODULE.toonContent)}
|
|
| 76 |
+ thumbnailPicker |
|
| 77 |
+ selectLabel={`${index + 1}컷 이미지 선택`}
|
|
| 78 |
+ thumbnailWidth="100%" |
|
| 79 |
+ thumbnailHeight={172}
|
|
| 80 |
+ defaultFiles={
|
|
| 81 |
+ cut.url |
|
| 82 |
+ ? [{ id: String(cut.key), name: `${index + 1}컷`, previewUrl: cut.url }]
|
|
| 83 |
+ : undefined |
|
| 84 |
+ } |
|
| 83 | 85 |
/> |
| 86 |
+ <input |
|
| 87 |
+ type="hidden" |
|
| 88 |
+ name="cutFileId" |
|
| 89 |
+ defaultValue={cut.url ? (retainedFileId ?? '') : ''}
|
|
| 90 |
+ /> |
|
| 91 |
+ <div className={styles.cutFooter}>
|
|
| 92 |
+ <span className={styles.cutLabel}>{index + 1}컷</span>
|
|
| 93 |
+ <span className={styles.cutActions}>
|
|
| 94 |
+ <FoxTextButton |
|
| 95 |
+ size="md" |
|
| 96 |
+ type="secondary" |
|
| 97 |
+ leadingIcon={<i className="fox-ico fox-ico-ArrowClockwise" aria-hidden="true" />}
|
|
| 98 |
+ label="초기화" |
|
| 99 |
+ onAction={() => resetCut(cut.key)}
|
|
| 100 |
+ /> |
|
| 101 |
+ <FoxTextButton |
|
| 102 |
+ size="md" |
|
| 103 |
+ type="secondary" |
|
| 104 |
+ leadingIcon={<i className="fox-ico fox-ico-Trash" aria-hidden="true" />}
|
|
| 105 |
+ label="삭제" |
|
| 106 |
+ disabled={cuts.length <= MIN_COMIC_CUTS}
|
|
| 107 |
+ onAction={() => removeCut(cut.key)}
|
|
| 108 |
+ /> |
|
| 109 |
+ </span> |
|
| 110 |
+ </div> |
|
| 84 | 111 |
</div> |
| 85 |
- </div> |
|
| 86 |
- ))} |
|
| 87 |
- </div> |
|
| 112 |
+ ))} |
|
| 113 |
+ </div> |
|
| 88 | 114 |
|
| 89 |
- <div> |
|
| 90 |
- <FoxTextButton |
|
| 91 |
- size="md" |
|
| 92 |
- leadingIcon={<i className="fox-ico fox-ico-Plus" aria-hidden="true" />}
|
|
| 93 |
- label="컷 추가" |
|
| 94 |
- onAction={addCut}
|
|
| 95 |
- /> |
|
| 115 |
+ <div className={styles.cutBoxFooter}>
|
|
| 116 |
+ <FoxTextButton |
|
| 117 |
+ size="md" |
|
| 118 |
+ leadingIcon={<i className="fox-ico fox-ico-Plus" aria-hidden="true" />}
|
|
| 119 |
+ label="컷 추가" |
|
| 120 |
+ onAction={addCut}
|
|
| 121 |
+ /> |
|
| 122 |
+ </div> |
|
| 96 | 123 |
</div> |
| 97 | 124 |
|
| 98 | 125 |
{error && <FoxHelperText type="danger" message={error} />}
|
--- app/(protected)/(basic)/contents/_components/content-form.module.scss
+++ app/(protected)/(basic)/contents/_components/content-form.module.scss
... | ... | @@ -59,31 +59,58 @@ |
| 59 | 59 |
min-inline-size: 0; |
| 60 | 60 |
} |
| 61 | 61 |
|
| 62 |
-.cuts {
|
|
| 62 |
+// 시안 tbl-form(5449:10857)의 슬롯 — 테두리 컨테이너 안에 회색 판(컷 카드들)과 흰 푸터(컷 추가). |
|
| 63 |
+.cutBox {
|
|
| 64 |
+ inline-size: 100%; |
|
| 65 |
+ overflow: hidden; |
|
| 66 |
+ border: fox.border(1) solid fox.color(border-neutral-default); |
|
| 67 |
+ border-radius: fox.radius(4); |
|
| 68 |
+ background: fox.color(surface-neutral-default); |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+.cutBody {
|
|
| 63 | 72 |
display: grid; |
| 64 | 73 |
grid-template-columns: repeat(4, minmax(0, 1fr)); |
| 65 | 74 |
gap: fox.gap(4); |
| 75 |
+ padding-block: fox.padding(5); |
|
| 76 |
+ padding-inline: fox.padding(6); |
|
| 77 |
+ background: fox.color(surface-neutral-gray-strong); |
|
| 66 | 78 |
} |
| 67 | 79 |
|
| 68 |
-.cut {
|
|
| 80 |
+.cutCard {
|
|
| 69 | 81 |
display: flex; |
| 70 | 82 |
flex-direction: column; |
| 71 |
- gap: fox.gap(2); |
|
| 83 |
+ gap: fox.gap(3); |
|
| 72 | 84 |
padding: fox.padding(3); |
| 73 |
- border: fox.border(1) solid fox.color(border-neutral-subtler); |
|
| 74 |
- border-radius: fox.radius(2); |
|
| 85 |
+ border-radius: fox.radius(4); |
|
| 86 |
+ background: fox.color(surface-neutral-default); |
|
| 87 |
+ box-shadow: |
|
| 88 |
+ fox.shadow(location1) fox.shadow(location1) fox.shadow(blur1) 0 |
|
| 89 |
+ fox.primitive(alpha-black10); |
|
| 75 | 90 |
} |
| 76 | 91 |
|
| 77 | 92 |
.cutFooter {
|
| 78 | 93 |
display: flex; |
| 79 | 94 |
align-items: center; |
| 80 | 95 |
justify-content: space-between; |
| 81 |
- gap: fox.gap(2); |
|
| 96 |
+ gap: fox.gap(3); |
|
| 82 | 97 |
} |
| 83 | 98 |
|
| 84 | 99 |
.cutLabel {
|
| 85 | 100 |
font-size: fox.font-size(label-sm); |
| 86 |
- color: fox.color(font-neutral-subtle); |
|
| 101 |
+ color: fox.color(font-neutral-default); |
|
| 102 |
+} |
|
| 103 |
+ |
|
| 104 |
+.cutActions {
|
|
| 105 |
+ display: flex; |
|
| 106 |
+ align-items: center; |
|
| 107 |
+ gap: fox.gap(3); |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 110 |
+.cutBoxFooter {
|
|
| 111 |
+ display: flex; |
|
| 112 |
+ padding-block: fox.padding(4); |
|
| 113 |
+ padding-inline: fox.padding(6); |
|
| 87 | 114 |
} |
| 88 | 115 |
|
| 89 | 116 |
.actions {
|
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?