--- app/(protected)/(basic)/boards/_actions.ts
+++ app/(protected)/(basic)/boards/_actions.ts
... | ... | @@ -8,10 +8,7 @@ |
| 8 | 8 |
saveInquiryAnswer, |
| 9 | 9 |
updateBoardPost, |
| 10 | 10 |
} from '@/lib/data/repositories/board-post-repository'; |
| 11 |
-import {
|
|
| 12 |
- MAX_ATTACHMENT_BYTES, |
|
| 13 |
- uploadAttachment, |
|
| 14 |
-} from '@/lib/data/repositories/file-repository'; |
|
| 11 |
+import { uploadAttachment } from '@/lib/data/repositories/file-repository';
|
|
| 15 | 12 |
import {
|
| 16 | 13 |
ANSWER_STATUS_DONE, |
| 17 | 14 |
type BoardType, |
... | ... | @@ -23,7 +20,10 @@ |
| 23 | 20 |
type InquiryAnswerFormState, |
| 24 | 21 |
} from '@/lib/domain/board-post-form'; |
| 25 | 22 |
import { BOARD_PATHS } from '@/lib/domain/board-post-query';
|
| 26 |
-import { FILE_MODULE_ID } from '@/lib/domain/file-module';
|
|
| 23 |
+import {
|
|
| 24 |
+ FILE_MODULE_ID, |
|
| 25 |
+ MAX_ATTACHMENT_BYTES, |
|
| 26 |
+} from '@/lib/domain/file-module'; |
|
| 27 | 27 |
|
| 28 | 28 |
/** |
| 29 | 29 |
* 게시판(공지사항·1:1문의·FAQ) 등록/수정/삭제/답변 Server Action. |
+++ app/(protected)/(basic)/boards/_components/board-attachment-field.module.scss
... | ... | @@ -0,0 +1,28 @@ |
| 1 | +@use "@fox/styles/abstracts" as fox; | |
| 2 | + | |
| 3 | +.input { | |
| 4 | + inline-size: 100%; | |
| 5 | + color: fox.color(font-neutral-subtle); | |
| 6 | + font-family: fox.font-family(body); | |
| 7 | + font-size: fox.font-size(body-sm); | |
| 8 | + | |
| 9 | + &::file-selector-button { | |
| 10 | + margin-inline-end: fox.gap(3); | |
| 11 | + padding-block: fox.padding(3); | |
| 12 | + padding-inline: fox.padding(5); | |
| 13 | + border: fox.border(1) solid fox.color(button-default-border); | |
| 14 | + border-radius: fox.form(radius-sm); | |
| 15 | + background: fox.color(button-default-surface); | |
| 16 | + color: fox.color(font-neutral-strong); | |
| 17 | + font-family: fox.font-family(body); | |
| 18 | + font-size: fox.font-size(label-sm); | |
| 19 | + font-weight: fox.font-weight(bold); | |
| 20 | + cursor: pointer; | |
| 21 | + } | |
| 22 | +} | |
| 23 | + | |
| 24 | +.existing { | |
| 25 | + color: fox.color(font-neutral-subtle); | |
| 26 | + font-family: fox.font-family(body); | |
| 27 | + font-size: fox.font-size(body-sm); | |
| 28 | +} |
--- app/(protected)/(basic)/boards/_components/board-attachment-field.tsx
+++ app/(protected)/(basic)/boards/_components/board-attachment-field.tsx
... | ... | @@ -3,7 +3,8 @@ |
| 3 | 3 |
import { useState } from 'react';
|
| 4 | 4 |
import { FoxHelperText } from '@fox/core/components/fox-helper-text';
|
| 5 | 5 |
import { Field } from '@/components/ui/field';
|
| 6 |
-import { MAX_ATTACHMENT_BYTES } from '@/lib/data/repositories/file-repository';
|
|
| 6 |
+import styles from './board-attachment-field.module.scss'; |
|
| 7 |
+import { MAX_ATTACHMENT_BYTES } from '@/lib/domain/file-module';
|
|
| 7 | 8 |
|
| 8 | 9 |
interface BoardAttachmentFieldProps {
|
| 9 | 10 |
label?: string; |
... | ... | @@ -42,7 +43,7 @@ |
| 42 | 43 |
<input type="hidden" name="attachmentId" value={attachmentId ?? ''} />
|
| 43 | 44 |
|
| 44 | 45 |
{attachmentId ? (
|
| 45 |
- <p className="text-body-sm text-foreground-muted"> |
|
| 46 |
+ <p className={styles.existing}>
|
|
| 46 | 47 |
기존 첨부파일{' '}
|
| 47 | 48 |
<a |
| 48 | 49 |
href={`${ATTACHMENT_DOWNLOAD_PATH}?atchFileId=${encodeURIComponent(attachmentId)}`}
|
... | ... | @@ -59,7 +60,7 @@ |
| 59 | 60 |
<input |
| 60 | 61 |
type="file" |
| 61 | 62 |
name="attachment" |
| 62 |
- className="w-full text-body-sm text-foreground file:mr-3 file:rounded-md file:border file:border-border file:bg-surface file:px-3 file:py-1.5 file:text-body-sm file:text-foreground" |
|
| 63 |
+ className={styles.input}
|
|
| 63 | 64 |
onChange={(event) => {
|
| 64 | 65 |
const file = event.target.files?.[0]; |
| 65 | 66 |
if (file && file.size > MAX_ATTACHMENT_BYTES) {
|
--- app/(protected)/(basic)/boards/_components/board-post-columns.tsx
+++ app/(protected)/(basic)/boards/_components/board-post-columns.tsx
... | ... | @@ -17,6 +17,7 @@ |
| 17 | 17 |
type BoardType, |
| 18 | 18 |
} from '@/lib/domain/board-post'; |
| 19 | 19 |
import { BoardRowActions } from './board-row-actions';
|
| 20 |
+import { InquiryRowActions } from '../inquiries/_components/inquiry-row-actions';
|
|
| 20 | 21 |
|
| 21 | 22 |
function numberColumn(page: number, pageSize: number): FoxListColumn<BoardPost> {
|
| 22 | 23 |
return {
|
... | ... | @@ -181,7 +182,12 @@ |
| 181 | 182 |
width: 140, |
| 182 | 183 |
render: (row) => formatOptionalValue(row.answeredAt), |
| 183 | 184 |
}, |
| 184 |
- actionsColumn('inquiry'),
|
|
| 185 |
+ {
|
|
| 186 |
+ key: 'actions', |
|
| 187 |
+ header: '관리', |
|
| 188 |
+ width: 160, |
|
| 189 |
+ render: (row) => <InquiryRowActions post={row} />,
|
|
| 190 |
+ }, |
|
| 185 | 191 |
]; |
| 186 | 192 |
} |
| 187 | 193 |
|
+++ app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.module.scss
... | ... | @@ -0,0 +1,37 @@ |
| 1 | +@use "@fox/styles/abstracts" as fox; | |
| 2 | + | |
| 3 | +.fields { | |
| 4 | + display: flex; | |
| 5 | + inline-size: 100%; | |
| 6 | + min-inline-size: 0; | |
| 7 | + flex-direction: column; | |
| 8 | +} | |
| 9 | + | |
| 10 | +.field { | |
| 11 | + display: flex; | |
| 12 | + inline-size: 100%; | |
| 13 | + flex-direction: column; | |
| 14 | + gap: fox.gap(3); | |
| 15 | + padding-block: fox.padding(6); | |
| 16 | + border-block-end: fox.border(1) solid fox.color(border-neutral-subtler); | |
| 17 | +} | |
| 18 | + | |
| 19 | +.pair { | |
| 20 | + display: flex; | |
| 21 | + align-items: flex-start; | |
| 22 | + gap: fox.gap(3); | |
| 23 | + | |
| 24 | + > * { | |
| 25 | + flex: 1 1 0; | |
| 26 | + min-inline-size: 0; | |
| 27 | + } | |
| 28 | +} | |
| 29 | + | |
| 30 | +.sectionTitle { | |
| 31 | + margin: 0; | |
| 32 | + padding-block-start: fox.padding(6); | |
| 33 | + color: fox.color(font-theme-primary); | |
| 34 | + font-family: fox.font-family(body); | |
| 35 | + font-size: fox.font-size(label-md); | |
| 36 | + font-weight: fox.font-weight(bold); | |
| 37 | +} |
--- app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.tsx
+++ app/(protected)/(basic)/boards/inquiries/_components/inquiry-answer-modal.tsx
... | ... | @@ -1,11 +1,13 @@ |
| 1 | 1 |
'use client'; |
| 2 | 2 |
|
| 3 |
-import { useActionState, useEffect } from 'react';
|
|
| 4 |
-import { Button } from '@/components/ui/button';
|
|
| 5 |
-import { Field } from '@/components/ui/field';
|
|
| 6 |
-import { Input } from '@/components/ui/input';
|
|
| 7 |
-import { Modal } from '@/components/ui/modal';
|
|
| 8 |
-import { Select } from '@/components/ui/select';
|
|
| 3 |
+import { useActionState, useEffect, useRef, useState } from 'react';
|
|
| 4 |
+import { FoxButton } from '@fox/core/components/fox-button';
|
|
| 5 |
+import { FoxHelperText } from '@fox/core/components/fox-helper-text';
|
|
| 6 |
+import { FoxInput } from '@fox/core/components/fox-input';
|
|
| 7 |
+import { FoxModal } from '@fox/core/components/fox-modal';
|
|
| 8 |
+import { FoxSelect } from '@fox/core/components/fox-select';
|
|
| 9 |
+import { FoxTextArea } from '@fox/core/components/fox-text-area';
|
|
| 10 |
+import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 9 | 11 |
import { useFeedback } from '@/app/_hooks/use-feedback';
|
| 10 | 12 |
import {
|
| 11 | 13 |
ANSWER_STATUS_OPTIONS, |
... | ... | @@ -19,35 +21,26 @@ |
| 19 | 21 |
} from '@/lib/domain/board-post-form'; |
| 20 | 22 |
import { saveInquiryAnswerAction } from '../../_actions';
|
| 21 | 23 |
import { BoardAttachmentField } from '../../_components/board-attachment-field';
|
| 22 |
-import { BoardContentField } from '../../_components/board-content-field';
|
|
| 23 |
-import { FoxHelperText } from '@fox/core/components/fox-helper-text';
|
|
| 24 |
-import { submitFormAction } from '@/app/_hooks/submit-form-action';
|
|
| 24 |
+import styles from './inquiry-answer-modal.module.scss'; |
|
| 25 | 25 |
|
| 26 | 26 |
interface InquiryAnswerModalProps {
|
| 27 | 27 |
post: BoardPost; |
| 28 | 28 |
onClose: () => void; |
| 29 | 29 |
} |
| 30 | 30 |
|
| 31 |
-const FORM_ID = 'inquiry-answer-form'; |
|
| 31 |
+const STATUS_HELP_TEXT = |
|
| 32 |
+ '답변완료 선택 시에만 FO(사용자 화면)에 답변 내용 노출, 목록에 답변일도 표시.'; |
|
| 32 | 33 |
|
| 33 | 34 |
/** |
| 34 |
- * 1:1문의 상세 및 답변 팝업 — 시안 A_BOA_013_p. |
|
| 35 |
- * |
|
| 36 |
- * 질문자가 작성한 항목(질문유형·제목·질문자명·작성일·이메일·전화번호·질문내용)은 **전부 읽기 |
|
| 37 |
- * 전용**이고, 관리자가 바꾸는 것은 진행상태·답변내용·답변첨부 셋뿐이다. 그래서 읽기 전용 값은 |
|
| 38 |
- * 폼에 싣지 않는다 — Server Action도 그 값들을 읽지 않으므로 위조해 보내도 반영되지 않는다. |
|
| 39 |
- * |
|
| 40 |
- * 전화번호는 **마스킹하지 않고 원본을 보여준다** — 목록에서는 마스킹하지만(시안 A_BOA_012), |
|
| 41 |
- * 상세는 답변을 위해 연락처를 확인하는 화면이라 시안에도 원본(`010-1234-5678`)으로 표기돼 있다. |
|
| 42 |
- * |
|
| 43 |
- * 첨부는 **답변 첨부만** 다룬다(사용자 확인 사항). 백엔드 필드가 `atchFileId` 하나뿐이라 질문 |
|
| 44 |
- * 첨부와 공유되며, 시안에도 "질문 첨부파일 → 일단 삭제" 메모가 있다. |
|
| 45 |
- * |
|
| 46 |
- * 시안 메모의 "답변완료 시 사용자에게 앱푸쉬 전송"은 **백엔드 미구현**이다 — 게시판 서비스의 푸시 |
|
| 47 |
- * 분기가 빈 블록이라 프론트가 할 수 있는 일이 없다. 별도로 보고한 사항이다. |
|
| 35 |
+ * 이슈: 기획의 "답변완료 시 앱푸쉬 전송"은 백엔드 푸시 분기가 빈 블록이라 동작하지 않는다. |
|
| 36 |
+ * 이슈: 백엔드 첨부 필드가 `atchFileId` 하나뿐이라 질문 첨부와 답변 첨부가 같은 자리를 쓴다. |
|
| 48 | 37 |
*/ |
| 49 | 38 |
export function InquiryAnswerModal({ post, onClose }: InquiryAnswerModalProps) {
|
| 50 | 39 |
const { showToast } = useFeedback();
|
| 40 |
+ const formRef = useRef<HTMLFormElement>(null); |
|
| 41 |
+ const [statusCode, setStatusCode] = useState( |
|
| 42 |
+ post.answerStatusCode ?? ANSWER_STATUS_OPTIONS[0].value |
|
| 43 |
+ ); |
|
| 51 | 44 |
const [state, formAction, isPending] = useActionState( |
| 52 | 45 |
saveInquiryAnswerAction, |
| 53 | 46 |
INITIAL_INQUIRY_ANSWER_FORM_STATE |
... | ... | @@ -63,106 +56,136 @@ |
| 63 | 56 |
const errors = state.status === 'error' ? (state.errors ?? {}) : {};
|
| 64 | 57 |
|
| 65 | 58 |
return ( |
| 66 |
- <Modal |
|
| 59 |
+ <FoxModal |
|
| 60 |
+ open |
|
| 61 |
+ size="md" |
|
| 67 | 62 |
title="1:1문의 상세 및 답변" |
| 68 | 63 |
onClose={onClose}
|
| 69 |
- footer={
|
|
| 64 |
+ actions={
|
|
| 70 | 65 |
<> |
| 71 |
- <Button type="button" variant="ghost" onClick={onClose}>
|
|
| 72 |
- 취소 |
|
| 73 |
- </Button> |
|
| 74 |
- <Button |
|
| 75 |
- type="submit" |
|
| 76 |
- form={FORM_ID}
|
|
| 77 |
- variant="primary" |
|
| 78 |
- disabled={isPending}
|
|
| 79 |
- > |
|
| 80 |
- {isPending ? '저장 중...' : '저장'}
|
|
| 81 |
- </Button> |
|
| 66 |
+ <FoxButton type="default" size="md" label="취소" onAction={onClose} />
|
|
| 67 |
+ <FoxButton |
|
| 68 |
+ type="primary" |
|
| 69 |
+ size="md" |
|
| 70 |
+ label="저장" |
|
| 71 |
+ loading={isPending}
|
|
| 72 |
+ onAction={() => formRef.current?.requestSubmit()}
|
|
| 73 |
+ /> |
|
| 82 | 74 |
</> |
| 83 | 75 |
} |
| 84 | 76 |
> |
| 85 |
- <form id={FORM_ID} onSubmit={(event) => submitFormAction(event, formAction)} className="flex flex-col gap-4">
|
|
| 77 |
+ <form |
|
| 78 |
+ ref={formRef}
|
|
| 79 |
+ onSubmit={(event) => submitFormAction(event, formAction)}
|
|
| 80 |
+ className={styles.fields}
|
|
| 81 |
+ > |
|
| 86 | 82 |
<input type="hidden" name="id" value={post.id} />
|
| 87 | 83 |
|
| 88 |
- <Field label="질문유형"> |
|
| 89 |
- <Input value={formatInquiryTypeLabel(post.typeCode)} readOnly />
|
|
| 90 |
- </Field> |
|
| 91 |
- |
|
| 92 |
- <p className="text-body-sm text-foreground-muted"> |
|
| 93 |
- 아래 질문 항목은 조회 전용입니다. |
|
| 94 |
- </p> |
|
| 95 |
- |
|
| 96 |
- <Field label="질문제목"> |
|
| 97 |
- <Input value={formatOptionalValue(post.title)} readOnly />
|
|
| 98 |
- </Field> |
|
| 99 |
- |
|
| 100 |
- <div className="flex gap-2"> |
|
| 101 |
- <Field label="질문자명" className="flex-1"> |
|
| 102 |
- <Input value={formatOptionalValue(post.authorName)} readOnly />
|
|
| 103 |
- </Field> |
|
| 104 |
- <Field label="작성일" className="flex-1"> |
|
| 105 |
- <Input value={formatOptionalValue(post.createdAt)} readOnly />
|
|
| 106 |
- </Field> |
|
| 84 |
+ <div className={styles.field}>
|
|
| 85 |
+ <FoxInput |
|
| 86 |
+ size="md" |
|
| 87 |
+ label="질문유형" |
|
| 88 |
+ value={formatInquiryTypeLabel(post.typeCode)}
|
|
| 89 |
+ readOnly |
|
| 90 |
+ /> |
|
| 107 | 91 |
</div> |
| 108 | 92 |
|
| 109 |
- <div className="flex gap-2"> |
|
| 110 |
- <Field label="이메일" className="flex-1"> |
|
| 111 |
- <Input value={formatOptionalValue(post.authorEmail)} readOnly />
|
|
| 112 |
- </Field> |
|
| 113 |
- <Field label="전화번호" className="flex-1"> |
|
| 114 |
- <Input value={formatOptionalValue(post.authorPhoneNumber)} readOnly />
|
|
| 115 |
- </Field> |
|
| 93 |
+ <p className={styles.sectionTitle}>게시물의 항목 (조회 전용)</p>
|
|
| 94 |
+ |
|
| 95 |
+ <div className={styles.field}>
|
|
| 96 |
+ <FoxInput |
|
| 97 |
+ size="md" |
|
| 98 |
+ label="질문제목" |
|
| 99 |
+ value={formatOptionalValue(post.title)}
|
|
| 100 |
+ readOnly |
|
| 101 |
+ /> |
|
| 116 | 102 |
</div> |
| 117 | 103 |
|
| 118 |
- <Field label="질문내용"> |
|
| 119 |
- <textarea |
|
| 104 |
+ <div className={styles.field}>
|
|
| 105 |
+ <div className={styles.pair}>
|
|
| 106 |
+ <FoxInput |
|
| 107 |
+ size="md" |
|
| 108 |
+ label="질문자명" |
|
| 109 |
+ value={formatOptionalValue(post.authorName)}
|
|
| 110 |
+ readOnly |
|
| 111 |
+ /> |
|
| 112 |
+ <FoxInput |
|
| 113 |
+ size="md" |
|
| 114 |
+ label="작성일" |
|
| 115 |
+ value={formatOptionalValue(post.createdAt)}
|
|
| 116 |
+ readOnly |
|
| 117 |
+ /> |
|
| 118 |
+ </div> |
|
| 119 |
+ </div> |
|
| 120 |
+ |
|
| 121 |
+ <div className={styles.field}>
|
|
| 122 |
+ <div className={styles.pair}>
|
|
| 123 |
+ <FoxInput |
|
| 124 |
+ size="md" |
|
| 125 |
+ label="이메일" |
|
| 126 |
+ value={formatOptionalValue(post.authorEmail)}
|
|
| 127 |
+ readOnly |
|
| 128 |
+ /> |
|
| 129 |
+ <FoxInput |
|
| 130 |
+ size="md" |
|
| 131 |
+ label="전화번호" |
|
| 132 |
+ value={formatOptionalValue(post.authorPhoneNumber)}
|
|
| 133 |
+ readOnly |
|
| 134 |
+ /> |
|
| 135 |
+ </div> |
|
| 136 |
+ </div> |
|
| 137 |
+ |
|
| 138 |
+ <div className={styles.field}>
|
|
| 139 |
+ <FoxTextArea |
|
| 140 |
+ label="질문내용" |
|
| 120 | 141 |
value={post.content}
|
| 121 | 142 |
readOnly |
| 122 |
- rows={5}
|
|
| 123 |
- className="w-full cursor-default rounded-md border border-border bg-surface-muted px-3 py-2 text-body-md text-foreground" |
|
| 143 |
+ rows={4}
|
|
| 124 | 144 |
/> |
| 125 |
- </Field> |
|
| 145 |
+ </div> |
|
| 126 | 146 |
|
| 127 |
- <Field label="진행상태 *"> |
|
| 128 |
- <Select |
|
| 129 |
- name="answerStatusCode" |
|
| 130 |
- defaultValue={post.answerStatusCode ?? ANSWER_STATUS_OPTIONS[0].value}
|
|
| 131 |
- > |
|
| 132 |
- {ANSWER_STATUS_OPTIONS.map((option) => (
|
|
| 133 |
- <option key={option.value} value={option.value}>
|
|
| 134 |
- {option.label}
|
|
| 135 |
- </option> |
|
| 136 |
- ))} |
|
| 137 |
- </Select> |
|
| 138 |
- </Field> |
|
| 139 |
- <p className="text-body-sm text-foreground-muted"> |
|
| 140 |
- 답변완료를 선택한 경우에만 사용자 화면에 답변 내용이 노출됩니다. |
|
| 141 |
- </p> |
|
| 142 |
- {errors.answerStatusCode && (
|
|
| 143 |
- <FoxHelperText type="danger" message={errors.answerStatusCode} />
|
|
| 144 |
- )} |
|
| 147 |
+ <div className={styles.field}>
|
|
| 148 |
+ <FoxSelect |
|
| 149 |
+ size="md" |
|
| 150 |
+ label="진행상태" |
|
| 151 |
+ requirement="required" |
|
| 152 |
+ options={ANSWER_STATUS_OPTIONS.map((option) => ({
|
|
| 153 |
+ value: option.value, |
|
| 154 |
+ label: option.label, |
|
| 155 |
+ }))} |
|
| 156 |
+ value={statusCode}
|
|
| 157 |
+ onValueChange={setStatusCode}
|
|
| 158 |
+ error={Boolean(errors.answerStatusCode)}
|
|
| 159 |
+ hint={errors.answerStatusCode ?? STATUS_HELP_TEXT}
|
|
| 160 |
+ /> |
|
| 161 |
+ <input type="hidden" name="answerStatusCode" value={statusCode} />
|
|
| 162 |
+ </div> |
|
| 145 | 163 |
|
| 146 |
- <BoardContentField |
|
| 147 |
- name="answerContent" |
|
| 148 |
- label="답변내용" |
|
| 149 |
- defaultValue={post.answerContent ?? ''}
|
|
| 150 |
- maxLength={ANSWER_MAX_LENGTH}
|
|
| 151 |
- placeholder="답변 내용을 입력하세요." |
|
| 152 |
- /> |
|
| 153 |
- {errors.answerContent && (
|
|
| 154 |
- <FoxHelperText type="danger" message={errors.answerContent} />
|
|
| 155 |
- )} |
|
| 164 |
+ <div className={styles.field}>
|
|
| 165 |
+ <FoxTextArea |
|
| 166 |
+ name="answerContent" |
|
| 167 |
+ label="답변내용" |
|
| 168 |
+ defaultValue={post.answerContent ?? ''}
|
|
| 169 |
+ maxLength={ANSWER_MAX_LENGTH}
|
|
| 170 |
+ counter |
|
| 171 |
+ rows={4}
|
|
| 172 |
+ placeholder="답변 내용을 입력하세요." |
|
| 173 |
+ invalid={Boolean(errors.answerContent)}
|
|
| 174 |
+ message={errors.answerContent}
|
|
| 175 |
+ /> |
|
| 176 |
+ </div> |
|
| 156 | 177 |
|
| 157 |
- <BoardAttachmentField |
|
| 158 |
- label="답변 첨부파일" |
|
| 159 |
- attachmentId={post.attachmentId}
|
|
| 160 |
- /> |
|
| 178 |
+ <div className={styles.field}>
|
|
| 179 |
+ <BoardAttachmentField |
|
| 180 |
+ label="답변 첨부파일" |
|
| 181 |
+ attachmentId={post.attachmentId}
|
|
| 182 |
+ /> |
|
| 183 |
+ </div> |
|
| 161 | 184 |
|
| 162 | 185 |
{state.status === 'error' && state.message && (
|
| 163 |
- <p className="text-body-sm text-danger">{state.message}</p>
|
|
| 186 |
+ <FoxHelperText type="danger" message={state.message} />
|
|
| 164 | 187 |
)} |
| 165 | 188 |
</form> |
| 166 |
- </Modal> |
|
| 189 |
+ </FoxModal> |
|
| 167 | 190 |
); |
| 168 | 191 |
} |
--- app/(protected)/(basic)/boards/inquiries/_components/inquiry-row-actions.tsx
+++ app/(protected)/(basic)/boards/inquiries/_components/inquiry-row-actions.tsx
... | ... | @@ -3,7 +3,8 @@ |
| 3 | 3 |
import { useState, useTransition } from 'react';
|
| 4 | 4 |
import { FoxButton } from '@fox/core/components/fox-button';
|
| 5 | 5 |
import { FoxButtonGroup } from '@fox/core/components/fox-button-group';
|
| 6 |
-import { Button } from '@/components/ui/button';
|
|
| 6 |
+import { FoxIconButton } from '@fox/core/components/fox-icon-button';
|
|
| 7 |
+import { FoxPencilSimpleIcon, FoxTrashIcon } from '@fox/core/icons';
|
|
| 7 | 8 |
import { useFeedback } from '@/app/_hooks/use-feedback';
|
| 8 | 9 |
import type { BoardPost } from '@/lib/domain/board-post';
|
| 9 | 10 |
import { deleteBoardPostAction } from '../../_actions';
|
... | ... | @@ -47,35 +48,33 @@ |
| 47 | 48 |
actions: ( |
| 48 | 49 |
<FoxButtonGroup size="md"> |
| 49 | 50 |
<FoxButton type="default" size="md" label="취소" onAction={hideAlert} />
|
| 50 |
- <FoxButton type="primary" size="md" label="삭제" onAction={runDelete} />
|
|
| 51 |
+ <FoxButton type="error" size="md" label="삭제" onAction={runDelete} />
|
|
| 51 | 52 |
</FoxButtonGroup> |
| 52 | 53 |
), |
| 53 | 54 |
}); |
| 54 | 55 |
} |
| 55 | 56 |
|
| 56 | 57 |
return ( |
| 57 |
- <div className="flex items-center gap-1"> |
|
| 58 |
- <Button |
|
| 59 |
- type="button" |
|
| 60 |
- variant="ghost" |
|
| 58 |
+ <FoxButtonGroup size="sm" align="center"> |
|
| 59 |
+ <FoxIconButton |
|
| 60 |
+ type="default" |
|
| 61 | 61 |
size="sm" |
| 62 |
- onClick={() => setIsAnswerOpen(true)}
|
|
| 63 |
- > |
|
| 64 |
- 답변 |
|
| 65 |
- </Button> |
|
| 66 |
- <Button |
|
| 67 |
- type="button" |
|
| 68 |
- variant="ghost" |
|
| 62 |
+ icon={<FoxPencilSimpleIcon />}
|
|
| 63 |
+ label="상세 및 답변" |
|
| 64 |
+ onAction={() => setIsAnswerOpen(true)}
|
|
| 65 |
+ /> |
|
| 66 |
+ <FoxIconButton |
|
| 67 |
+ type="default" |
|
| 69 | 68 |
size="sm" |
| 70 |
- onClick={confirmDelete}
|
|
| 69 |
+ icon={<FoxTrashIcon />}
|
|
| 70 |
+ label={isDeleting ? '삭제 중' : '삭제'}
|
|
| 71 | 71 |
disabled={isDeleting}
|
| 72 |
- > |
|
| 73 |
- {isDeleting ? '삭제 중...' : '삭제'}
|
|
| 74 |
- </Button> |
|
| 72 |
+ onAction={confirmDelete}
|
|
| 73 |
+ /> |
|
| 75 | 74 |
|
| 76 | 75 |
{isAnswerOpen && (
|
| 77 | 76 |
<InquiryAnswerModal post={post} onClose={() => setIsAnswerOpen(false)} />
|
| 78 | 77 |
)} |
| 79 |
- </div> |
|
| 78 |
+ </FoxButtonGroup> |
|
| 80 | 79 |
); |
| 81 | 80 |
} |
--- lib/data/repositories/file-repository.ts
+++ lib/data/repositories/file-repository.ts
... | ... | @@ -22,8 +22,6 @@ |
| 22 | 22 |
|
| 23 | 23 |
const FILE_UPLOAD_PATH = '/api/v1/common/file/upload'; |
| 24 | 24 |
|
| 25 |
-/** 업로드 상한. 백엔드에 명시적 제한이 보이지 않아 프론트에서 방어적으로 건다. */ |
|
| 26 |
-export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; |
|
| 27 | 25 |
|
| 28 | 26 |
/** 파일 생성은 일반 조회보다 오래 걸릴 수 있어 넉넉히 잡는다. */ |
| 29 | 27 |
const UPLOAD_TIMEOUT_MS = 60_000; |
--- lib/domain/file-module.ts
+++ lib/domain/file-module.ts
... | ... | @@ -7,3 +7,6 @@ |
| 7 | 7 |
} as const; |
| 8 | 8 |
|
| 9 | 9 |
export type FileModuleId = (typeof FILE_MODULE_ID)[keyof typeof FILE_MODULE_ID]; |
| 10 |
+ |
|
| 11 |
+/** 첨부파일 상한. 화면(사전 차단)과 Server Action(재검증)이 같은 값을 본다. */ |
|
| 12 |
+export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; |
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?