feat(WEB): 결재함 문서 상세 모달 데이터 연동 개선

- ApprovalBox: 문서 클릭 시 API 데이터 로드하여 모달에 표시
- DocumentCreate: 품의서 폼 개선 및 actions 수정
- 결재자 정보 (직책, 부서) 표시 개선
This commit is contained in:
2026-01-22 23:19:37 +09:00
parent 2d3a7064e3
commit c8890c1a85
6 changed files with 213 additions and 85 deletions

View File

@@ -1,5 +1,6 @@
'use client';
import { useEffect, useState } from 'react';
import { Mic } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -8,14 +9,61 @@ import { CurrencyInput } from '@/components/ui/currency-input';
import { Textarea } from '@/components/ui/textarea';
import { FileDropzone } from '@/components/ui/file-dropzone';
import { FileList, type NewFile, type ExistingFile } from '@/components/ui/file-list';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { getClients } from '@/components/accounting/VendorManagement/actions';
import type { ProposalData, UploadedFile } from './types';
// 거래처 옵션 타입
interface ClientOption {
id: string;
name: string;
}
interface ProposalFormProps {
data: ProposalData;
onChange: (data: ProposalData) => void;
}
export function ProposalForm({ data, onChange }: ProposalFormProps) {
// 거래처 목록 상태
const [clients, setClients] = useState<ClientOption[]>([]);
const [isLoadingClients, setIsLoadingClients] = useState(true);
// 거래처 목록 로드 (매입 거래처만)
useEffect(() => {
async function loadClients() {
setIsLoadingClients(true);
const result = await getClients({ size: 1000, only_active: true });
if (result.success) {
// 매입 거래처(purchase, both)만 필터링
const purchaseClients = result.data
.filter((v) => v.category === 'purchase' || v.category === 'both')
.map((v) => ({
id: v.id,
name: v.vendorName,
}));
setClients(purchaseClients);
}
setIsLoadingClients(false);
}
loadClients();
}, []);
// 거래처 선택 핸들러
const handleVendorChange = (vendorId: string) => {
const selected = clients.find((c) => c.id === vendorId);
onChange({
...data,
vendorId,
vendor: selected?.name || '',
});
};
const handleFilesSelect = (files: File[]) => {
onChange({ ...data, attachments: [...data.attachments, ...files] });
};
@@ -41,12 +89,22 @@ export function ProposalForm({ data, onChange }: ProposalFormProps) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="vendor"></Label>
<Input
id="vendor"
placeholder="구매처를 입력해주세요"
value={data.vendor}
onChange={(e) => onChange({ ...data, vendor: e.target.value })}
/>
<Select
value={data.vendorId || ''}
onValueChange={handleVendorChange}
disabled={isLoadingClients}
>
<SelectTrigger>
<SelectValue placeholder={isLoadingClients ? '불러오는 중...' : '구매처를 선택해주세요'} />
</SelectTrigger>
<SelectContent>
{clients.map((client) => (
<SelectItem key={client.id} value={client.id}>
{client.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">

View File

@@ -760,7 +760,8 @@ function transformApiToFormData(apiData: {
department,
};
if (step.step_type === 'approval') {
// 'approval'과 'agreement' 모두 결재선에 포함
if (step.step_type === 'approval' || step.step_type === 'agreement') {
approvalLine.push(person);
} else if (step.step_type === 'reference') {
references.push(person);
@@ -808,6 +809,7 @@ function transformApiToFormData(apiData: {
if (documentType === 'proposal') {
proposalData = {
vendorId: (content.vendorId as string) || '',
vendor: (content.vendor as string) || '',
vendorPaymentDate: (content.vendorPaymentDate as string) || '',
title: (content.title as string) || '',
@@ -894,6 +896,7 @@ function getDocumentContent(
switch (formData.basicInfo.documentType) {
case 'proposal':
return {
vendorId: formData.proposalData?.vendorId,
vendor: formData.proposalData?.vendor,
vendorPaymentDate: formData.proposalData?.vendorPaymentDate,
title: formData.proposalData?.title,

View File

@@ -45,6 +45,7 @@ import type {
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { useDevFill, generatePurchaseApprovalData } from '@/components/dev';
import { getClients } from '@/components/accounting/VendorManagement/actions';
// 초기 데이터 - SSR에서는 빈 문자열, 클라이언트에서 날짜 설정
const getInitialBasicInfo = (): BasicInfo => ({
@@ -55,6 +56,7 @@ const getInitialBasicInfo = (): BasicInfo => ({
});
const getInitialProposalData = (): ProposalData => ({
vendorId: '',
vendor: '',
vendorPaymentDate: '', // 클라이언트에서 설정
title: '',
@@ -131,6 +133,19 @@ export function DocumentCreate() {
// 직원 목록 가져오기
const employees = await getEmployees();
// 거래처 목록 가져오기 (매입 거래처만)
const clientsResult = await getClients({ size: 1000, only_active: true });
const purchaseClients = clientsResult.success
? clientsResult.data
.filter((v) => v.category === 'purchase' || v.category === 'both')
.map((v) => ({ id: v.id, name: v.vendorName }))
: [];
// 랜덤 거래처 선택
const randomClient = purchaseClients.length > 0
? purchaseClients[Math.floor(Math.random() * purchaseClients.length)]
: null;
// localStorage에서 실제 로그인 사용자 이름 가져오기 (우측 상단 표시와 동일한 소스)
const userDataStr = localStorage.getItem("user");
const currentUserName = userDataStr ? JSON.parse(userDataStr).name : currentUser?.name;
@@ -171,6 +186,9 @@ export function DocumentCreate() {
setProposalData(prev => ({
...prev,
...mockData.proposalData,
// 실제 API 거래처로 덮어쓰기
vendorId: randomClient?.id || '',
vendor: randomClient?.name || '',
}));
toast.success('지출결의서 데이터가 자동 입력되었습니다.');
}

View File

@@ -38,7 +38,8 @@ export interface BasicInfo {
// 품의서 데이터
export interface ProposalData {
vendor: string;
vendorId: string; // 거래처 ID (API 연동)
vendor: string; // 거래처명 (표시용)
vendorPaymentDate: string;
title: string;
description: string;