From 07aaa32bdf64e62d2b46571035631132625fd580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Tue, 27 Jan 2026 14:47:28 +0900 Subject: [PATCH] =?UTF-8?q?fix(WEB):=20E2E=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20(HOTFIX=202026-01-?= =?UTF-8?q?27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카드내역 일괄변경 시 선택 항목 인식 안되는 버그 수정 - 게시판 글쓰기/수정 폼 미렌더링 버그 수정 (mode=new/edit 처리) - 자동 출퇴근 설정 저장 안되는 버그 수정 (useAuto API 연동) - DynamicBoardCreateForm/EditForm 컴포넌트 분리 - UniversalListPage에 onSelectionChange 콜백 추가 Co-Authored-By: Claude Opus 4.5 --- ...OTFIX-2026-01-27] E2E-테스트-수정계획서.md | 286 ++++++++++++++++++ .../boards/[boardCode]/[postId]/edit/page.tsx | 240 +-------------- .../boards/[boardCode]/[postId]/page.tsx | 32 +- .../boards/[boardCode]/create/page.tsx | 154 +--------- .../(protected)/boards/[boardCode]/page.tsx | 32 +- .../DynamicBoard/DynamicBoardCreateForm.tsx | 165 ++++++++++ .../DynamicBoard/DynamicBoardEditForm.tsx | 252 +++++++++++++++ .../AttendanceSettingsManagement/actions.ts | 6 +- .../AttendanceSettingsManagement/index.tsx | 5 +- .../templates/UniversalListPage/index.tsx | 7 + .../templates/UniversalListPage/types.ts | 2 + 11 files changed, 786 insertions(+), 395 deletions(-) create mode 100644 claudedocs/[HOTFIX-2026-01-27] E2E-테스트-수정계획서.md create mode 100644 src/components/board/DynamicBoard/DynamicBoardCreateForm.tsx create mode 100644 src/components/board/DynamicBoard/DynamicBoardEditForm.tsx diff --git a/claudedocs/[HOTFIX-2026-01-27] E2E-테스트-수정계획서.md b/claudedocs/[HOTFIX-2026-01-27] E2E-테스트-수정계획서.md new file mode 100644 index 00000000..dbb6bdb0 --- /dev/null +++ b/claudedocs/[HOTFIX-2026-01-27] E2E-테스트-수정계획서.md @@ -0,0 +1,286 @@ +# E2E 테스트 기반 프론트엔드 수정 계획서 + +**작성일**: 2026-01-27 +**기준**: sam-hotfix 프로젝트 1월 27일 E2E 테스트 결과 +**대상**: sam-react-prod (Next.js 프론트엔드) + +--- + +## 📊 테스트 결과 요약 + +| 구분 | 개수 | 비율 | +|------|------|------| +| ✅ PASS | 44 | 93.6% | +| ⚠️ PARTIAL PASS | 3 | 6.4% | +| ❌ FAIL | 0 | 0% | + +--- + +## 🔴 HIGH 우선순위 수정 항목 + +### 1. BUG-CARDTRANS-001: 카드내역 일괄변경 선택 항목 인식 안됨 + +**위치**: `/accounting/card-transactions` +**컴포넌트**: `src/components/accounting/CardTransactionInquiry/` +**심각도**: HIGH + +**증상**: +- 테이블 전체선택 체크박스 클릭 → "6개 항목 선택됨" 표시 +- 계정과목명 드롭다운에서 "경비" 선택 +- 저장 버튼 클릭 +- **예상**: "6개의 사용 유형을 경비(으)로 변경하시겠습니까?" 확인 다이얼로그 +- **실제**: "항목 선택 필요 - 변경할 카드 사용 내역을 먼저 선택해주세요." 오류 + +**원인 분석**: +- 일괄변경 저장 시 `selectedItems` 상태가 올바르게 전달되지 않음 +- 또는 저장 핸들러에서 선택 항목 체크 로직 오류 + +**수정 방향**: +```typescript +// 확인 필요한 부분 +1. selectedItems 상태가 일괄변경 컴포넌트에 올바르게 전달되는지 +2. 저장 핸들러에서 selectedItems.size > 0 체크 로직 +3. UniversalListPage 또는 IntegratedListTemplateV2의 선택 상태 동기화 +``` + +**예상 수정 파일**: +- `src/components/accounting/CardTransactionInquiry/index.tsx` +- `src/components/templates/UniversalListPage/index.tsx` (선택 상태 관련) + +--- + +### 2. BUG-BOARD-001: 게시판 글쓰기 폼 미렌더링 + +**위치**: `/boards/{board_id}` +**컴포넌트**: 동적 게시판 페이지 +**심각도**: HIGH + +**증상**: +- 게시판 목록 페이지에서 "글쓰기" 버튼 클릭 +- URL이 `?mode=new`로 변경됨 +- **예상**: 게시글 작성 폼 표시 (제목, 내용 입력 필드) +- **실제**: 목록 화면 그대로 유지 + +**원인 분석**: +- `mode=new` URL 파라미터 감지 후 폼 렌더링 로직 누락 +- 또는 조건부 렌더링 로직 오류 + +**수정 방향**: +```typescript +// 확인 필요한 부분 +const searchParams = useSearchParams(); +const mode = searchParams.get('mode'); + +// mode === 'new' 일 때 작성 폼 렌더링 +if (mode === 'new') { + return ; +} +``` + +**예상 수정 파일**: +- `src/app/[locale]/(protected)/boards/[boardId]/page.tsx` +- 또는 해당 게시판 컴포넌트 + +--- + +### 3. BUG-BOARD-002: 게시판 수정 폼 미렌더링 + +**위치**: `/boards/{board_id}/{post_id}` +**컴포넌트**: 게시판 상세 페이지 +**심각도**: HIGH + +**증상**: +- 게시글 상세 페이지에서 "수정" 버튼 클릭 +- URL이 `?mode=edit`로 변경됨 +- **예상**: 게시글 편집 폼 표시 (기존 내용 로드) +- **실제**: 상세보기 화면 그대로 유지 + +**원인 분석**: +- `mode=edit` URL 파라미터 감지 후 편집 폼 렌더링 로직 누락 +- 또는 조건부 렌더링 로직 오류 + +**수정 방향**: +```typescript +// 확인 필요한 부분 +const mode = searchParams.get('mode'); + +// mode === 'edit' 일 때 편집 폼 렌더링 +if (mode === 'edit') { + return ; +} +``` + +**예상 수정 파일**: +- `src/app/[locale]/(protected)/boards/[boardId]/[postId]/page.tsx` +- 또는 해당 게시판 상세 컴포넌트 + +--- + +## 🟡 MEDIUM 우선순위 수정 항목 + +### 4. BUG-ATTSETTING-001: 자동 출퇴근 설정 저장 안됨 + +**위치**: `/settings/attendance-settings` +**컴포넌트**: `src/components/settings/AttendanceSettings/` (추정) +**심각도**: MEDIUM + +**증상**: +- GPS 출퇴근 활성화 → 부서 선택 → 반경 300M 설정 +- 자동 출퇴근 활성화 +- 저장 버튼 클릭 +- 페이지 새로고침 +- **예상**: 자동 출퇴근 체크박스 ON 상태 유지 +- **실제**: 자동 출퇴근 체크박스 OFF로 초기화됨 +- **참고**: GPS 출퇴근 설정(체크박스, 반경)은 정상 저장됨 + +**원인 분석**: +- 자동 출퇴근 설정값이 API 호출에 포함되지 않음 +- 또는 백엔드에서 해당 필드 저장 누락 + +**수정 방향**: +```typescript +// 저장 API 호출 시 payload 확인 +const savePayload = { + gpsEnabled: true, + gpsRadius: 300, + autoPunchEnabled: true, // ← 이 값이 포함되는지 확인 + ... +}; +``` + +**예상 수정 파일**: +- `src/components/settings/AttendanceSettings/index.tsx` +- `src/components/settings/AttendanceSettings/actions.ts` + +--- + +## 🟢 LOW 우선순위 수정 항목 + +### 5. BUG-ATTSETTING-002: 저장 완료 토스트 미표시 + +**위치**: `/settings/attendance-settings` +**심각도**: LOW + +**증상**: +- 저장 버튼 클릭 시 "출퇴근 설정이 저장되었습니다." 토스트 미표시 +- 콘솔 에러 없음, URL 유지됨 + +**수정 방향**: +```typescript +// 저장 성공 후 토스트 추가 +import { toast } from 'sonner'; + +const handleSave = async () => { + const result = await saveSettings(payload); + if (result.success) { + toast.success('출퇴근 설정이 저장되었습니다.'); + } +}; +``` + +--- + +## 📋 작업 체크리스트 + +### Phase 1: HIGH 우선순위 (즉시 수정) ✅ 완료 + +- [x] **카드내역 일괄변경 버그 수정** (2026-01-27 수정 완료) + - [x] CardTransactionInquiry 컴포넌트 분석 + - [x] selectedItems 상태 흐름 추적 + - [x] UniversalListPage에 onSelectionChange 콜백 추가 + - [x] 테스트 검증 + +- [x] **게시판 글쓰기 폼 렌더링 수정** (2026-01-27 수정 완료) + - [x] 동적 게시판 페이지 구조 분석 + - [x] mode=new 파라미터 처리 로직 추가 + - [x] DynamicBoardCreateForm 컴포넌트 분리 및 연결 + - [x] 테스트 검증 + +- [x] **게시판 수정 폼 렌더링 수정** (2026-01-27 수정 완료) + - [x] 게시판 상세 페이지 구조 분석 + - [x] mode=edit 파라미터 처리 로직 추가 + - [x] DynamicBoardEditForm 컴포넌트 분리 및 연결 + - [x] 테스트 검증 + +### Phase 2: MEDIUM 우선순위 ✅ 완료 + +- [x] **자동 출퇴근 설정 저장 버그 수정** (2026-01-27 수정 완료) + - [x] 저장 API payload 분석 + - [x] 백엔드 DB에 use_auto 컬럼 추가 (마이그레이션) + - [x] 백엔드 Model/Request 수정 + - [x] 프론트엔드 API 연동 수정 + - [x] 테스트 검증 + +### Phase 3: LOW 우선순위 ✅ 완료 + +- [x] **저장 완료 토스트 추가** (이미 구현되어 있음) + - [x] 저장 핸들러에 toast.success 이미 존재 (index.tsx:142) + +--- + +## 📌 추가 확인 필요 사항 + +### 백엔드 이슈 (프론트 수정 범위 외) + +| 이슈 | 위치 | 상태 | +|------|------|------| +| reference-box 500 에러 | `/api/v1/boards/reference` | 백엔드 확인 필요 | + +### 기획 변경 사항 + +| 항목 | 변경 내용 | 조치 | +|------|----------|------| +| payment-history | 구독관리 페이지로 통합됨 | 테스트 시나리오 삭제/수정 | + +--- + +## 🔍 참고: 이미 수정된 항목 + +### bank-transactions key 중복 오류 (2026-01-27 수정 완료) + +**수정 내용**: 입금/출금 통합 목록에서 React key 중복 오류 +**수정 파일**: `src/components/accounting/BankTransactionInquiry/actions.ts` +**수정 방법**: ID 생성 시 거래 유형을 접두어로 추가 +```typescript +// 기존: id: String(item.id) +// 수정: id: `${item.type}-${item.id}` +``` + +### BUG-CARDTRANS-001: 카드내역 일괄변경 선택 인식 (2026-01-27 수정 완료) + +**수정 내용**: UniversalListPage에서 선택 상태 변경 시 외부 콜백 호출 +**수정 파일**: +- `src/components/templates/UniversalListPage/types.ts` - onSelectionChange 타입 추가 +- `src/components/templates/UniversalListPage/index.tsx` - useEffect로 콜백 호출 추가 + +### BUG-BOARD-001/002: 게시판 글쓰기/수정 폼 미렌더링 (2026-01-27 수정 완료) + +**수정 내용**: mode=new/edit URL 파라미터 처리 로직 추가 +**신규 컴포넌트**: +- `src/components/board/DynamicBoard/DynamicBoardCreateForm.tsx` - 글쓰기 폼 +- `src/components/board/DynamicBoard/DynamicBoardEditForm.tsx` - 수정 폼 + +**수정 파일**: +- `src/app/[locale]/(protected)/boards/[boardCode]/page.tsx` - mode=new 처리 +- `src/app/[locale]/(protected)/boards/[boardCode]/[postId]/page.tsx` - mode=edit 처리 +- `src/app/[locale]/(protected)/boards/[boardCode]/create/page.tsx` - 컴포넌트 재사용 +- `src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx` - 컴포넌트 재사용 + +### BUG-ATTSETTING-001: 자동 출퇴근 설정 저장 안됨 (2026-01-27 수정 완료) + +**원인**: 백엔드 DB에 use_auto 컬럼이 없었음 + +**백엔드 수정 (sam-api)**: +- `database/migrations/2026_01_27_144110_add_use_auto_to_attendance_settings.php` - 마이그레이션 생성 +- `app/Models/Tenants/AttendanceSetting.php` - fillable, casts에 use_auto 추가 +- `app/Http/Requests/V1/WorkSetting/UpdateAttendanceSettingRequest.php` - validation 규칙 추가 + +**프론트엔드 수정 (sam-react-prod)**: +- `src/components/settings/AttendanceSettingsManagement/actions.ts` - API 타입 및 transform 함수 수정 +- `src/components/settings/AttendanceSettingsManagement/index.tsx` - 로드/저장 시 useAuto 필드 연동 + +--- + +**작성자**: Claude Code +**상태**: 전체 완료 ✅ +**백엔드 배포 필요**: sam-api 프로젝트에서 `php artisan migrate` 실행 필요 \ No newline at end of file diff --git a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx index c9605c3a..52621227 100644 --- a/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx +++ b/src/app/[locale]/(protected)/boards/[boardCode]/[postId]/edit/page.tsx @@ -2,248 +2,16 @@ /** * 동적 게시판 수정 페이지 + * DynamicBoardEditForm 컴포넌트를 사용 */ -import { useState, useEffect } from 'react'; -import { useRouter, useParams } from 'next/navigation'; -import { ArrowLeft, Save, MessageSquare } from 'lucide-react'; -import { DetailPageSkeleton } from '@/components/ui/skeleton'; -import { PageLayout } from '@/components/organisms/PageLayout'; -import { PageHeader } from '@/components/organisms/PageHeader'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { Checkbox } from '@/components/ui/checkbox'; -import { getDynamicBoardPost, updateDynamicBoardPost } from '@/components/board/DynamicBoard/actions'; -import { getBoardByCode } from '@/components/board/BoardManagement/actions'; -import type { PostApiData } from '@/components/customer-center/shared/types'; - -interface BoardPost { - id: string; - title: string; - content: string; - authorId: string; - authorName: string; - status: string; - views: number; - isNotice: boolean; - isSecret: boolean; - createdAt: string; - updatedAt: string; -} - -// API 데이터 → 프론트엔드 타입 변환 -function transformApiToPost(apiData: PostApiData): BoardPost { - return { - id: String(apiData.id), - title: apiData.title, - content: apiData.content, - authorId: String(apiData.user_id), - authorName: apiData.author?.name || '회원', - status: apiData.status, - views: apiData.views, - isNotice: apiData.is_notice, - isSecret: apiData.is_secret, - createdAt: apiData.created_at, - updatedAt: apiData.updated_at, - }; -} +import { useParams } from 'next/navigation'; +import { DynamicBoardEditForm } from '@/components/board/DynamicBoard/DynamicBoardEditForm'; export default function DynamicBoardEditPage() { - const router = useRouter(); const params = useParams(); const boardCode = params.boardCode as string; const postId = params.postId as string; - // 게시판 정보 - const [boardName, setBoardName] = useState('게시판'); - - // 원본 게시글 - const [post, setPost] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [loadError, setLoadError] = useState(null); - - // 폼 상태 - const [title, setTitle] = useState(''); - const [content, setContent] = useState(''); - const [isSecret, setIsSecret] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(null); - - // 게시판 정보 로드 - useEffect(() => { - async function fetchBoardInfo() { - const result = await getBoardByCode(boardCode); - if (result.success && result.data) { - setBoardName(result.data.boardName); - } - } - fetchBoardInfo(); - }, [boardCode]); - - // 게시글 로드 - useEffect(() => { - async function fetchPost() { - setIsLoading(true); - setLoadError(null); - - const result = await getDynamicBoardPost(boardCode, postId); - - if (result.success && result.data) { - const postData = transformApiToPost(result.data); - setPost(postData); - setTitle(postData.title); - setContent(postData.content); - setIsSecret(postData.isSecret); - } else { - setLoadError(result.error || '게시글을 찾을 수 없습니다.'); - } - - setIsLoading(false); - } - - fetchPost(); - }, [boardCode, postId]); - - // 폼 제출 - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!title.trim()) { - setError('제목을 입력해주세요.'); - return; - } - - if (!content.trim()) { - setError('내용을 입력해주세요.'); - return; - } - - setIsSubmitting(true); - setError(null); - - const result = await updateDynamicBoardPost(boardCode, postId, { - title: title.trim(), - content: content.trim(), - is_secret: isSecret, - }); - - if (result.success) { - router.push(`/ko/boards/${boardCode}/${postId}`); - } else { - setError(result.error || '게시글 수정에 실패했습니다.'); - setIsSubmitting(false); - } - }; - - // 취소 - const handleCancel = () => { - router.push(`/ko/boards/${boardCode}/${postId}`); - }; - - // 로딩 상태 - if (isLoading) { - return ( - - - - ); - } - - // 로드 에러 - if (loadError || !post) { - return ( - -
-

{loadError || '게시글을 찾을 수 없습니다.'}

- -
-
- ); - } - - return ( - - - -
- - - 게시글 수정 - - - {error && ( -
-

{error}

-
- )} - - {/* 제목 */} -
- - setTitle(e.target.value)} - placeholder="제목을 입력하세요" - disabled={isSubmitting} - /> -
- - {/* 내용 */} -
- -