임동욱 임동욱 08-20
fix: 게시판 수정·답변을 단건 조회로 연다
findBoardPostById가 있는데 아무도 부르지 않고 목록 행으로 폼을 채우고 있었다.
행은 화면에 떠 있는 동안 낡을 수 있고, 그대로 저장하면 남의 수정을 덮는다.

없어진 게시물이면 얼럿으로 알리고 팝업을 열지 않는다.

Co-Authored-By: Claude Opus 5 
@f5e8b09ff0b06ca1096354b10908de07aa986c7b
app/(protected)/(basic)/boards/_actions.ts
--- app/(protected)/(basic)/boards/_actions.ts
+++ app/(protected)/(basic)/boards/_actions.ts
@@ -7,12 +7,14 @@
 import {
   createBoardPost,
   deleteBoardPost,
+  findBoardPostById,
   saveInquiryAnswer,
   updateBoardPost,
 } from '@/lib/data/repositories/board-post-repository';
 import { uploadAttachment } from '@/lib/data/repositories/file-repository';
 import {
   ANSWER_STATUS_DONE,
+  type BoardPost,
   type BoardType,
 } from '@/lib/domain/board-post';
 import {
@@ -56,6 +58,8 @@
 )}MB 이하만 올릴 수 있습니다.`;
 const SAVE_FAILED_MESSAGE = '저장하지 못했습니다. 잠시 후 다시 시도해 주세요.';
 const DELETE_FAILED_MESSAGE = '삭제하지 못했습니다. 잠시 후 다시 시도해 주세요.';
+const LOAD_FAILED_MESSAGE = '게시물을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.';
+const GONE_MESSAGE = '이미 삭제된 게시물입니다. 목록을 새로고침해 주세요.';
 
 /** FAQ 유형 허용 목록. 화면이 보낸 코드를 그대로 믿지 않기 위해 저장 직전에 다시 조회한다. */
 async function fetchFaqTypeCodes(boardType: BoardType): Promise<string[]> {
@@ -284,6 +288,28 @@
  * 폼 제출이 아니라 얼럿의 [삭제] 클릭에 반응하는 단발 호출이라 `useActionState`의
  * (prevState, formData) 규약 대신 값을 직접 받는다.
  */
+/**
+ * 수정·답변 팝업이 열릴 때 그 게시물을 다시 읽는다 — 목록 행은 화면에 떠 있는 동안 남이
+ * 고쳤을 수 있어, 그것으로 폼을 채우면 오래된 값을 그대로 덮어쓴다.
+ */
+export async function loadBoardPostAction(
+  bbsId: string
+): Promise<
+  { status: 'success'; post: BoardPost } | { status: 'error'; message: string }
+> {
+  await verifySession();
+
+  try {
+    const post = await findBoardPostById(bbsId);
+    if (!post) {
+      return { status: 'error', message: GONE_MESSAGE };
+    }
+    return { status: 'success', post };
+  } catch {
+    return { status: 'error', message: LOAD_FAILED_MESSAGE };
+  }
+}
+
 export async function deleteBoardPostAction(
   boardType: BoardType,
   bbsId: string
app/(protected)/(basic)/boards/_components/board-row-actions.tsx
--- app/(protected)/(basic)/boards/_components/board-row-actions.tsx
+++ app/(protected)/(basic)/boards/_components/board-row-actions.tsx
@@ -7,7 +7,7 @@
 import { useFeedback } from '@/app/_hooks/use-feedback';
 import type { BoardPost, BoardType } from '@/lib/domain/board-post';
 import type { CommonCode } from '@/lib/domain/common-code';
-import { deleteBoardPostAction } from '../_actions';
+import { deleteBoardPostAction, loadBoardPostAction } from '../_actions';
 import { BoardPostFormModal } from './board-post-form-modal';
 
 interface BoardRowActionsProps {
@@ -35,8 +35,21 @@
   faqTypes,
 }: BoardRowActionsProps) {
   const { showAlert, hideAlert, showToast } = useFeedback();
-  const [isEditOpen, setIsEditOpen] = useState(false);
+  // 목록 행이 아니라 단건 조회 결과로 폼을 채운다 — 행은 화면에 떠 있는 동안 낡을 수 있다.
+  const [editPost, setEditPost] = useState<BoardPost | null>(null);
+  const [isLoading, startLoading] = useTransition();
   const [isDeleting, startDeleting] = useTransition();
+
+  function openEdit() {
+    startLoading(async () => {
+      const result = await loadBoardPostAction(post.id);
+      if (result.status === 'error') {
+        showToast({ variant: 'danger', message: result.message });
+        return;
+      }
+      setEditPost(result.post);
+    });
+  }
 
   function runDelete() {
     hideAlert();
@@ -73,8 +86,9 @@
         type="default"
         size="sm"
         icon={<i className="fox-ico fox-ico-PencilSimple" aria-hidden="true" />}
-        label="수정"
-        onAction={() => setIsEditOpen(true)}
+        label={isLoading ? '불러오는 중' : '수정'}
+        disabled={isLoading}
+        onAction={openEdit}
       />
       <FoxIconButton
         type="default"
@@ -85,13 +99,13 @@
         onAction={confirmDelete}
       />
 
-      {isEditOpen && (
+      {editPost && (
         <BoardPostFormModal
           boardType={boardType}
           mode="edit"
           faqTypes={faqTypes}
-          post={post}
-          onClose={() => setIsEditOpen(false)}
+          post={editPost}
+          onClose={() => setEditPost(null)}
         />
       )}
     </div>
app/(protected)/(basic)/boards/inquiries/_components/inquiry-row-actions.tsx
--- app/(protected)/(basic)/boards/inquiries/_components/inquiry-row-actions.tsx
+++ app/(protected)/(basic)/boards/inquiries/_components/inquiry-row-actions.tsx
@@ -7,7 +7,7 @@
 import { useFeedback } from '@/app/_hooks/use-feedback';
 import type { BoardPost } from '@/lib/domain/board-post';
 import type { CommonCode } from '@/lib/domain/common-code';
-import { deleteBoardPostAction } from '../../_actions';
+import { deleteBoardPostAction, loadBoardPostAction } from '../../_actions';
 import { InquiryAnswerModal } from './inquiry-answer-modal';
 
 interface InquiryRowActionsProps {
@@ -26,8 +26,21 @@
   answerStatuses,
 }: InquiryRowActionsProps) {
   const { showAlert, hideAlert, showToast } = useFeedback();
-  const [isAnswerOpen, setIsAnswerOpen] = useState(false);
+  // 목록 행이 아니라 단건 조회 결과로 채운다 — 답변 중에 남이 고쳤을 수 있다.
+  const [answerPost, setAnswerPost] = useState<BoardPost | null>(null);
+  const [isLoading, startLoading] = useTransition();
   const [isDeleting, startDeleting] = useTransition();
+
+  function openAnswer() {
+    startLoading(async () => {
+      const result = await loadBoardPostAction(post.id);
+      if (result.status === 'error') {
+        showToast({ variant: 'danger', message: result.message });
+        return;
+      }
+      setAnswerPost(result.post);
+    });
+  }
 
   function runDelete() {
     hideAlert();
@@ -64,8 +77,9 @@
         type="default"
         size="sm"
         icon={<i className="fox-ico fox-ico-PencilSimple" aria-hidden="true" />}
-        label="상세 및 답변"
-        onAction={() => setIsAnswerOpen(true)}
+        label={isLoading ? '불러오는 중' : '상세 및 답변'}
+        disabled={isLoading}
+        onAction={openAnswer}
       />
       <FoxIconButton
         type="default"
@@ -76,11 +90,11 @@
         onAction={confirmDelete}
       />
 
-      {isAnswerOpen && (
+      {answerPost && (
         <InquiryAnswerModal
-          post={post}
+          post={answerPost}
           answerStatuses={answerStatuses}
-          onClose={() => setIsAnswerOpen(false)}
+          onClose={() => setAnswerPost(null)}
         />
       )}
     </FoxButtonGroup>
Add a comment
List