feat(WEB): 회계/HR/주문관리 모듈 개선 및 알림설정 리팩토링

- 회계: 거래처, 매입/매출, 입출금 상세 페이지 개선
- HR: 직원 관리 및 출퇴근 설정 기능 수정
- 주문관리: 상세폼 구조 분리 (cards, dialogs, hooks, tables)
- 알림설정: 컴포넌트 구조 단순화 및 리팩토링
- 캘린더: 헤더 및 일정 타입 개선
- 출고관리: 액션 및 타입 정의 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2026-01-06 09:58:10 +09:00
parent 386cd30bc0
commit a938da9e22
76 changed files with 2899 additions and 2073 deletions

View File

@@ -125,6 +125,15 @@ function transformFrontendToApi(data: AttendanceFormData): Record<string, unknow
};
}
/**
* 분을 휴게시간 문자열로 변환 (예: 90 -> "1:30")
*/
function formatMinutesToBreakTime(minutes: number): string {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}:${mins.toString().padStart(2, '0')}`;
}
/**
* 분을 시간 문자열로 변환 (예: 210 -> "3시간 30분")
*/

View File

@@ -34,7 +34,7 @@ import {
EMPLOYEE_STATUS_LABELS,
DEFAULT_FIELD_SETTINGS,
} from './types';
import { getPositions, getDepartments, type PositionItem, type DepartmentItem } from './actions';
import { getPositions, getDepartments, uploadProfileImage, type PositionItem, type DepartmentItem } from './actions';
import { getProfileImageUrl } from './utils';
interface EmployeeFormProps {
@@ -464,8 +464,10 @@ export function EmployeeForm({
if (file) {
// 미리보기 즉시 표시
handleChange('profileImage', URL.createObjectURL(file));
// 서버에 업로드
const result = await uploadProfileImage(file);
// 서버에 업로드 (FormData로 감싸서 전송)
const formData = new FormData();
formData.append('file', file);
const result = await uploadProfileImage(formData);
if (result.success && result.data?.url) {
handleChange('profileImage', result.data.url);
}

View File

@@ -16,7 +16,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import { serverFetch, getServerApiHeaders } from '@/lib/api/fetch-wrapper';
import type { Employee, EmployeeFormData, EmployeeStats } from './types';
import { transformApiToFrontend, transformFrontendToApi, type EmployeeApiData } from './utils';
@@ -365,7 +365,7 @@ export interface PositionItem {
*/
export async function getPositions(type?: 'rank' | 'title'): Promise<PositionItem[]> {
try {
const headers = await getApiHeaders();
const headers = await getServerApiHeaders();
const searchParams = new URLSearchParams();
if (type) {
searchParams.set('type', type);
@@ -414,7 +414,7 @@ export interface DepartmentItem {
*/
export async function getDepartments(): Promise<DepartmentItem[]> {
try {
const headers = await getApiHeaders();
const headers = await getServerApiHeaders();
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments`,
@@ -449,7 +449,7 @@ export async function getDepartments(): Promise<DepartmentItem[]> {
// 파일 업로드
// ============================================
export async function uploadProfileImage(formData: FormData): Promise<{
export async function uploadProfileImage(inputFormData: FormData): Promise<{
success: boolean;
data?: { url: string; path: string };
error?: string;
@@ -464,9 +464,8 @@ export async function uploadProfileImage(formData: FormData): Promise<{
return { success: false, __authError: true };
}
const formData = new FormData();
formData.append('file', file);
formData.append('directory', 'employees/profiles');
// 디렉토리 정보 추가
inputFormData.append('directory', 'employees/profiles');
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/files/upload`,
@@ -476,7 +475,7 @@ export async function uploadProfileImage(formData: FormData): Promise<{
'Authorization': `Bearer ${token}`,
'X-API-KEY': process.env.API_KEY || '',
},
body: formData,
body: inputFormData,
}
);