fix(WEB): E2E 테스트 버그 수정 (HOTFIX 2026-01-27)

- 카드내역 일괄변경 시 선택 항목 인식 안되는 버그 수정
- 게시판 글쓰기/수정 폼 미렌더링 버그 수정 (mode=new/edit 처리)
- 자동 출퇴근 설정 저장 안되는 버그 수정 (useAuto API 연동)
- DynamicBoardCreateForm/EditForm 컴포넌트 분리
- UniversalListPage에 onSelectionChange 콜백 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-27 14:47:28 +09:00
parent 815ed9267e
commit 07aaa32bdf
11 changed files with 786 additions and 395 deletions

View File

@@ -0,0 +1,165 @@
'use client';
/**
* 동적 게시판 등록 폼 컴포넌트
* - mode=new 또는 /create 페이지에서 사용
*/
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { ArrowLeft, Save, MessageSquare } from 'lucide-react';
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 { createDynamicBoardPost } from '@/components/board/DynamicBoard/actions';
import { getBoardByCode } from '@/components/board/BoardManagement/actions';
interface DynamicBoardCreateFormProps {
boardCode: string;
}
export function DynamicBoardCreateForm({ boardCode }: DynamicBoardCreateFormProps) {
const router = useRouter();
// 게시판 정보
const [boardName, setBoardName] = useState<string>('게시판');
// 폼 상태
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [isSecret, setIsSecret] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
// 게시판 정보 로드
useEffect(() => {
async function fetchBoardInfo() {
const result = await getBoardByCode(boardCode);
if (result.success && result.data) {
setBoardName(result.data.boardName);
}
}
fetchBoardInfo();
}, [boardCode]);
// 폼 제출
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 createDynamicBoardPost(boardCode, {
title: title.trim(),
content: content.trim(),
is_secret: isSecret,
});
if (result.success && result.data) {
router.push(`/ko/boards/${boardCode}/${result.data.id}`);
} else {
setError(result.error || '게시글 등록에 실패했습니다.');
setIsSubmitting(false);
}
};
// 취소
const handleCancel = () => {
router.push(`/ko/boards/${boardCode}`);
};
return (
<PageLayout>
<PageHeader
title={boardName}
description="새 게시글 등록"
icon={MessageSquare}
/>
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-md">
<p className="text-sm text-destructive">{error}</p>
</div>
)}
{/* 제목 */}
<div className="space-y-2">
<Label htmlFor="title"> *</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="제목을 입력하세요"
disabled={isSubmitting}
/>
</div>
{/* 내용 */}
<div className="space-y-2">
<Label htmlFor="content"> *</Label>
<Textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="내용을 입력하세요"
rows={15}
disabled={isSubmitting}
/>
</div>
{/* 비밀글 설정 */}
<div className="flex items-center space-x-2">
<Checkbox
id="isSecret"
checked={isSecret}
onCheckedChange={(checked) => setIsSecret(checked as boolean)}
disabled={isSubmitting}
/>
<Label htmlFor="isSecret" className="font-normal cursor-pointer">
</Label>
</div>
</CardContent>
</Card>
{/* 버튼 영역 */}
<div className="mt-6 flex items-center justify-between">
<Button
type="button"
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
>
<ArrowLeft className="h-4 w-4 mr-2" />
</Button>
<Button type="submit" disabled={isSubmitting}>
<Save className="h-4 w-4 mr-2" />
{isSubmitting ? '등록 중...' : '등록'}
</Button>
</div>
</form>
</PageLayout>
);
}

View File

@@ -0,0 +1,252 @@
'use client';
/**
* 동적 게시판 수정 폼 컴포넌트
* - mode=edit 또는 /edit 페이지에서 사용
*/
import { useState, useEffect } from 'react';
import { useRouter } 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,
};
}
interface DynamicBoardEditFormProps {
boardCode: string;
postId: string;
}
export function DynamicBoardEditForm({ boardCode, postId }: DynamicBoardEditFormProps) {
const router = useRouter();
// 게시판 정보
const [boardName, setBoardName] = useState<string>('게시판');
// 원본 게시글
const [post, setPost] = useState<BoardPost | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
// 폼 상태
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [isSecret, setIsSecret] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(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 (
<PageLayout>
<DetailPageSkeleton />
</PageLayout>
);
}
// 로드 에러
if (loadError || !post) {
return (
<PageLayout>
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
<p className="text-muted-foreground">{loadError || '게시글을 찾을 수 없습니다.'}</p>
<Button variant="outline" onClick={() => router.push(`/ko/boards/${boardCode}`)}>
<ArrowLeft className="h-4 w-4 mr-2" />
</Button>
</div>
</PageLayout>
);
}
return (
<PageLayout>
<PageHeader
title={boardName}
description="게시글 수정"
icon={MessageSquare}
/>
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<div className="p-3 bg-destructive/10 border border-destructive/20 rounded-md">
<p className="text-sm text-destructive">{error}</p>
</div>
)}
{/* 제목 */}
<div className="space-y-2">
<Label htmlFor="title"> *</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="제목을 입력하세요"
disabled={isSubmitting}
/>
</div>
{/* 내용 */}
<div className="space-y-2">
<Label htmlFor="content"> *</Label>
<Textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="내용을 입력하세요"
rows={15}
disabled={isSubmitting}
/>
</div>
{/* 비밀글 설정 */}
<div className="flex items-center space-x-2">
<Checkbox
id="isSecret"
checked={isSecret}
onCheckedChange={(checked) => setIsSecret(checked as boolean)}
disabled={isSubmitting}
/>
<Label htmlFor="isSecret" className="font-normal cursor-pointer">
</Label>
</div>
</CardContent>
</Card>
{/* 버튼 영역 */}
<div className="mt-6 flex items-center justify-between">
<Button
type="button"
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
>
<ArrowLeft className="h-4 w-4 mr-2" />
</Button>
<Button type="submit" disabled={isSubmitting}>
<Save className="h-4 w-4 mr-2" />
{isSubmitting ? '저장 중...' : '저장'}
</Button>
</div>
</form>
</PageLayout>
);
}