feat: fetchWrapper 마이그레이션 및 토큰 리프레시 캐싱 구현

- 40+ actions.ts 파일을 fetchWrapper 패턴으로 마이그레이션
- 토큰 리프레시 캐싱 로직 추가 (refresh-token.ts)
- ApiErrorContext 추가로 전역 에러 처리 개선
- HR EmployeeForm 컴포넌트 개선
- 참조함(ReferenceBox) 기능 수정
- juil 테스트 URL 페이지 추가
- claudedocs 문서 업데이트

🤖 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
2025-12-30 17:00:18 +09:00
parent 0e5307f7a3
commit d38b1242d7
82 changed files with 7434 additions and 4775 deletions

View File

@@ -13,8 +13,8 @@
'use server';
import { cookies } from 'next/headers';
import { revalidatePath } from 'next/cache';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { BadDebtRecord, CollectionStatus } from './types';
// ============================================
@@ -118,21 +118,6 @@ interface BadDebtSummaryApiData {
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* API 상태 → 프론트엔드 상태 변환
* API: legal_action, bad_debt (snake_case)
@@ -267,7 +252,6 @@ export async function getBadDebts(params?: {
client_id?: string;
}): Promise<BadDebtRecord[]> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -281,14 +265,15 @@ export async function getBadDebts(params?: {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts?${searchParams.toString()}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.error('[BadDebtActions] GET list error:', response.status);
if (error) {
console.error('[BadDebtActions] GET list error:', error.message);
return [];
}
if (!response?.ok) {
console.error('[BadDebtActions] GET list error:', response?.status);
return [];
}
@@ -311,19 +296,18 @@ export async function getBadDebts(params?: {
*/
export async function getBadDebtById(id: string): Promise<BadDebtRecord | null> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET' }
);
if (!response.ok) {
console.error('[BadDebtActions] GET detail error:', response.status);
if (error) {
console.error('[BadDebtActions] GET detail error:', error.message);
return null;
}
if (!response?.ok) {
console.error('[BadDebtActions] GET detail error:', response?.status);
return null;
}
@@ -345,19 +329,18 @@ export async function getBadDebtById(id: string): Promise<BadDebtRecord | null>
*/
export async function getBadDebtSummary(): Promise<BadDebtSummaryApiData | null> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/summary`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET' }
);
if (!response.ok) {
console.error('[BadDebtActions] GET summary error:', response.status);
if (error) {
console.error('[BadDebtActions] GET summary error:', error.message);
return null;
}
if (!response?.ok) {
console.error('[BadDebtActions] GET summary error:', response?.status);
return null;
}
@@ -381,26 +364,28 @@ export async function createBadDebt(
data: Partial<BadDebtRecord>
): Promise<{ success: boolean; data?: BadDebtRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[BadDebtActions] POST request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '악성채권 등록에 실패했습니다.',
error: result?.message || '악성채권 등록에 실패했습니다.',
};
}
@@ -427,26 +412,28 @@ export async function updateBadDebt(
data: Partial<BadDebtRecord>
): Promise<{ success: boolean; data?: BadDebtRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[BadDebtActions] PUT request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '악성채권 수정에 실패했습니다.',
error: result?.message || '악성채권 수정에 실패했습니다.',
};
}
@@ -470,22 +457,21 @@ export async function updateBadDebt(
*/
export async function deleteBadDebt(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}`,
{
method: 'DELETE',
headers,
}
{ method: 'DELETE' }
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '악성채권 삭제에 실패했습니다.',
error: result?.message || '악성채권 삭제에 실패했습니다.',
};
}
@@ -506,22 +492,21 @@ export async function deleteBadDebt(id: string): Promise<{ success: boolean; err
*/
export async function toggleBadDebt(id: string): Promise<{ success: boolean; data?: BadDebtRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${id}/toggle`,
{
method: 'PATCH',
headers,
}
{ method: 'PATCH' }
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '상태 변경에 실패했습니다.',
error: result?.message || '상태 변경에 실패했습니다.',
};
}
@@ -548,23 +533,24 @@ export async function addBadDebtMemo(
content: string
): Promise<{ success: boolean; data?: { id: string; content: string; createdAt: string; createdBy: string }; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${badDebtId}/memos`,
{
method: 'POST',
headers,
body: JSON.stringify({ content }),
}
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '메모 추가에 실패했습니다.',
error: result?.message || '메모 추가에 실패했습니다.',
};
}
@@ -595,22 +581,21 @@ export async function deleteBadDebtMemo(
memoId: string
): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bad-debts/${badDebtId}/memos/${memoId}`,
{
method: 'DELETE',
headers,
}
{ method: 'DELETE' }
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '메모 삭제에 실패했습니다.',
error: result?.message || '메모 삭제에 실패했습니다.',
};
}
@@ -622,4 +607,4 @@ export async function deleteBadDebtMemo(
error: '서버 오류가 발생했습니다.',
};
}
}
}

View File

@@ -1,21 +1,8 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { BankTransaction, TransactionKind } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface BankTransactionApiItem {
id: number;
@@ -101,7 +88,6 @@ export async function getBankTransactionList(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -117,19 +103,24 @@ export async function getBankTransactionList(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-transactions${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[BankTransactionActions] GET bank-transactions error:', response.status);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
error: error.message,
};
}
if (!response?.ok) {
console.warn('[BankTransactionActions] GET bank-transactions error:', response?.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response?.status}`,
};
}
@@ -189,7 +180,6 @@ export async function getBankTransactionSummary(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.startDate) searchParams.set('start_date', params.startDate);
@@ -198,17 +188,17 @@ export async function getBankTransactionSummary(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-transactions/summary${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[BankTransactionActions] GET summary error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.warn('[BankTransactionActions] GET summary error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -248,21 +238,20 @@ export async function getBankAccountOptions(): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-transactions/accounts`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[BankTransactionActions] GET accounts error:', response.status);
if (error) {
return { success: false, data: [], error: error.message };
}
if (!response?.ok) {
console.warn('[BankTransactionActions] GET accounts error:', response?.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -288,4 +277,4 @@ export async function getBankAccountOptions(): Promise<{
error: '서버 오류가 발생했습니다.',
};
}
}
}

View File

@@ -1,23 +1,11 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { BillRecord, BillApiData, BillStatus } from './types';
import { transformApiToFrontend, transformFrontendToApi } from './types';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 어음 목록 조회 =====
export async function getBills(params: {
search?: string;
@@ -43,9 +31,9 @@ export async function getBills(params: {
total: number;
};
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const queryParams = new URLSearchParams();
if (params.search) queryParams.append('search', params.search);
@@ -62,11 +50,29 @@ export async function getBills(params: {
if (params.perPage) queryParams.append('per_page', String(params.perPage));
if (params.page) queryParams.append('page', String(params.page));
const response = await fetch(
const { response, error } = await serverFetch(
`${API_URL}/api/v1/bills?${queryParams.toString()}`,
{ method: 'GET', headers, cache: 'no-store' }
{ method: 'GET' }
);
if (error?.__authError) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
__authError: true,
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: error?.message || 'Failed to fetch bills',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -112,15 +118,22 @@ export async function getBill(id: string): Promise<{
success: boolean;
data?: BillRecord;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${API_URL}/api/v1/bills/${id}`,
{ method: 'GET', headers, cache: 'no-store' }
{ method: 'GET' }
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || 'Failed to fetch bill' };
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -140,27 +153,32 @@ export async function getBill(id: string): Promise<{
// ===== 어음 등록 =====
export async function createBill(
data: Partial<BillRecord>
): Promise<{ success: boolean; data?: BillRecord; error?: string }> {
): Promise<{ success: boolean; data?: BillRecord; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[createBill] Sending data:', JSON.stringify(apiData, null, 2));
const response = await fetch(
const { response, error } = await serverFetch(
`${API_URL}/api/v1/bills`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || 'Failed to create bill' };
}
const result = await response.json();
console.log('[createBill] Response:', result);
if (!response.ok || !result.success) {
// 유효성 검사 에러 처리
if (result.errors) {
const errorMessages = Object.values(result.errors).flat().join(', ');
return { success: false, error: errorMessages || result.message || 'Failed to create bill' };
@@ -182,27 +200,32 @@ export async function createBill(
export async function updateBill(
id: string,
data: Partial<BillRecord>
): Promise<{ success: boolean; data?: BillRecord; error?: string }> {
): Promise<{ success: boolean; data?: BillRecord; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[updateBill] Sending data:', JSON.stringify(apiData, null, 2));
const response = await fetch(
const { response, error } = await serverFetch(
`${API_URL}/api/v1/bills/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || 'Failed to update bill' };
}
const result = await response.json();
console.log('[updateBill] Response:', result);
if (!response.ok || !result.success) {
// 유효성 검사 에러 처리
if (result.errors) {
const errorMessages = Object.values(result.errors).flat().join(', ');
return { success: false, error: errorMessages || result.message || 'Failed to update bill' };
@@ -221,15 +244,21 @@ export async function updateBill(
}
// ===== 어음 삭제 =====
export async function deleteBill(id: string): Promise<{ success: boolean; error?: string }> {
export async function deleteBill(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${API_URL}/api/v1/bills/${id}`,
{ method: 'DELETE', headers }
{ method: 'DELETE' }
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || 'Failed to delete bill' };
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -247,19 +276,24 @@ export async function deleteBill(id: string): Promise<{ success: boolean; error?
export async function updateBillStatus(
id: string,
status: BillStatus
): Promise<{ success: boolean; data?: BillRecord; error?: string }> {
): Promise<{ success: boolean; data?: BillRecord; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${API_URL}/api/v1/bills/${id}/status`,
{
method: 'PATCH',
headers,
body: JSON.stringify({ status }),
}
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || 'Failed to update bill status' };
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -293,9 +327,9 @@ export async function getBillSummary(params: {
maturityAlertAmount: number;
};
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const queryParams = new URLSearchParams();
if (params.billType && params.billType !== 'all') queryParams.append('bill_type', params.billType);
@@ -304,11 +338,19 @@ export async function getBillSummary(params: {
if (params.maturityStartDate) queryParams.append('maturity_start_date', params.maturityStartDate);
if (params.maturityEndDate) queryParams.append('maturity_end_date', params.maturityEndDate);
const response = await fetch(
const { response, error } = await serverFetch(
`${API_URL}/api/v1/bills/summary?${queryParams.toString()}`,
{ method: 'GET', headers, cache: 'no-store' }
{ method: 'GET' }
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || 'Failed to fetch summary' };
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -336,15 +378,22 @@ export async function getClients(): Promise<{
success: boolean;
data?: { id: number; name: string }[];
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${API_URL}/api/v1/clients?per_page=100`,
{ method: 'GET', headers, cache: 'no-store' }
{ method: 'GET' }
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || 'Failed to fetch clients' };
}
const result = await response.json();
if (!response.ok || !result.success) {

View File

@@ -1,21 +1,8 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { CardTransaction } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface CardTransactionApiItem {
id: number;
@@ -105,7 +92,6 @@ export async function getCardTransactionList(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -120,19 +106,24 @@ export async function getCardTransactionList(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/card-transactions${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[CardTransactionActions] GET card-transactions error:', response.status);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
error: error.message,
};
}
if (!response?.ok) {
console.warn('[CardTransactionActions] GET card-transactions error:', response?.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response?.status}`,
};
}
@@ -192,7 +183,6 @@ export async function getCardTransactionSummary(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.startDate) searchParams.set('start_date', params.startDate);
@@ -201,17 +191,17 @@ export async function getCardTransactionSummary(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/card-transactions/summary${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[CardTransactionActions] GET summary error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.warn('[CardTransactionActions] GET summary error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -254,20 +244,22 @@ export async function bulkUpdateAccountCode(
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/card-transactions/bulk-update-account`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'PUT',
headers,
body: JSON.stringify({ ids, account_code: accountCode }),
});
if (!response.ok) {
console.warn('[CardTransactionActions] PUT bulk-update-account error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.warn('[CardTransactionActions] PUT bulk-update-account error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -291,4 +283,4 @@ export async function bulkUpdateAccountCode(
error: '서버 오류가 발생했습니다.',
};
}
}
}

View File

@@ -1,21 +1,9 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { NoteReceivableItem, DailyAccountItem, MatchStatus } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface NoteReceivableItemApi {
id: string;
@@ -89,7 +77,6 @@ export async function getNoteReceivables(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.date) searchParams.set('date', params.date);
@@ -97,18 +84,18 @@ export async function getNoteReceivables(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/daily-report/note-receivables${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[DailyReportActions] GET note-receivables error:', response.status);
if (error) {
return { success: false, data: [], error: error.message };
}
if (!response?.ok) {
console.warn('[DailyReportActions] GET note-receivables error:', response?.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -147,7 +134,6 @@ export async function getDailyAccounts(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.date) searchParams.set('date', params.date);
@@ -155,18 +141,18 @@ export async function getDailyAccounts(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/daily-report/daily-accounts${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[DailyReportActions] GET daily-accounts error:', response.status);
if (error) {
return { success: false, data: [], error: error.message };
}
if (!response?.ok) {
console.warn('[DailyReportActions] GET daily-accounts error:', response?.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -223,7 +209,6 @@ export async function getDailyReportSummary(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.date) searchParams.set('date', params.date);
@@ -231,17 +216,17 @@ export async function getDailyReportSummary(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/daily-report/summary${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[DailyReportActions] GET summary error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.warn('[DailyReportActions] GET summary error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -331,4 +316,4 @@ export async function exportDailyReportExcel(params?: {
error: '서버 오류가 발생했습니다.',
};
}
}
}

View File

@@ -1,21 +1,8 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { DepositRecord, DepositType, DepositStatus } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface DepositApiData {
id: number;
@@ -60,6 +47,23 @@ function transformApiToFrontend(apiData: DepositApiData): DepositRecord {
};
}
// ===== Frontend → API 변환 =====
function transformFrontendToApi(data: Partial<DepositRecord>): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (data.depositDate !== undefined) result.deposit_date = data.depositDate;
if (data.depositAmount !== undefined) result.deposit_amount = data.depositAmount;
if (data.accountName !== undefined) result.account_name = data.accountName;
if (data.depositorName !== undefined) result.depositor_name = data.depositorName;
if (data.note !== undefined) result.note = data.note || null;
if (data.depositType !== undefined) result.deposit_type = data.depositType;
if (data.vendorId !== undefined) result.vendor_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
if (data.vendorName !== undefined) result.vendor_name = data.vendorName || null;
if (data.status !== undefined) result.status = data.status;
return result;
}
// ===== 입금 내역 조회 =====
export async function getDeposits(params?: {
page?: number;
@@ -80,122 +84,102 @@ export async function getDeposits(params?: {
};
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.depositType && params.depositType !== 'all') {
searchParams.set('deposit_type', params.depositType);
}
if (params?.vendor && params.vendor !== 'all') {
searchParams.set('vendor', params.vendor);
}
if (params?.search) searchParams.set('search', params.search);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.depositType && params.depositType !== 'all') {
searchParams.set('deposit_type', params.depositType);
}
if (params?.vendor && params.vendor !== 'all') {
searchParams.set('vendor', params.vendor);
}
if (params?.search) searchParams.set('search', params.search);
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[DepositActions] GET deposits error:', response.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '입금 내역 조회에 실패했습니다.',
};
}
// API 응답 구조 처리: { data: { data: [...], current_page: ... } } 또는 { data: [...], meta: {...} }
const isPaginatedResponse = result.data && typeof result.data === 'object' && 'data' in result.data && Array.isArray(result.data.data);
const rawData = isPaginatedResponse ? result.data.data : (Array.isArray(result.data) ? result.data : []);
const deposits = rawData.map(transformApiToFrontend);
const meta: PaginationMeta = isPaginatedResponse
? {
current_page: result.data.current_page || 1,
last_page: result.data.last_page || 1,
per_page: result.data.per_page || 20,
total: result.data.total || deposits.length,
}
: result.meta || {
current_page: 1,
last_page: 1,
per_page: 20,
total: deposits.length,
};
return {
success: true,
data: deposits,
pagination: {
currentPage: meta.current_page,
lastPage: meta.last_page,
perPage: meta.per_page,
total: meta.total,
},
};
} catch (error) {
console.error('[DepositActions] getDeposits error:', error);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '서버 오류가 발생했습니다.',
error: error.message,
};
}
if (!response?.ok) {
console.warn('[DepositActions] GET deposits error:', response?.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response?.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '입금 내역 조회에 실패했습니다.',
};
}
// API 응답 구조 처리: { data: { data: [...], current_page: ... } } 또는 { data: [...], meta: {...} }
const isPaginatedResponse = result.data && typeof result.data === 'object' && 'data' in result.data && Array.isArray(result.data.data);
const rawData = isPaginatedResponse ? result.data.data : (Array.isArray(result.data) ? result.data : []);
const deposits = rawData.map(transformApiToFrontend);
const meta: PaginationMeta = isPaginatedResponse
? {
current_page: result.data.current_page || 1,
last_page: result.data.last_page || 1,
per_page: result.data.per_page || 20,
total: result.data.total || deposits.length,
}
: result.meta || {
current_page: 1,
last_page: 1,
per_page: 20,
total: deposits.length,
};
return {
success: true,
data: deposits,
pagination: {
currentPage: meta.current_page,
lastPage: meta.last_page,
perPage: meta.per_page,
total: meta.total,
},
};
}
// ===== 입금 내역 삭제 =====
export async function deleteDeposit(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`;
const { response, error } = await serverFetch(url, { method: 'DELETE' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`,
{
method: 'DELETE',
headers,
}
);
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '입금 내역 삭제에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[DepositActions] deleteDeposit error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '입금 내역 삭제에 실패했습니다.' };
}
return { success: true };
}
// ===== 계정과목명 일괄 저장 =====
@@ -203,38 +187,26 @@ export async function updateDepositTypes(
ids: string[],
depositType: string
): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/bulk-update-type`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify({
ids: ids.map(id => parseInt(id, 10)),
deposit_type: depositType,
}),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/bulk-update-type`,
{
method: 'PUT',
headers,
body: JSON.stringify({
ids: ids.map(id => parseInt(id, 10)),
deposit_type: depositType,
}),
}
);
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '계정과목명 저장에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[DepositActions] updateDepositTypes error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '계정과목명 저장에 실패했습니다.' };
}
return { success: true };
}
// ===== 입금 상세 조회 =====
@@ -243,105 +215,52 @@ export async function getDepositById(id: string): Promise<{
data?: DepositRecord;
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.error('[DepositActions] GET deposit error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '입금 내역 조회에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[DepositActions] getDepositById error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
}
// ===== Frontend → API 변환 =====
function transformFrontendToApi(data: Partial<DepositRecord>): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (!response?.ok) {
console.error('[DepositActions] GET deposit error:', response?.status);
return { success: false, error: `API 오류: ${response?.status}` };
}
if (data.depositDate !== undefined) result.deposit_date = data.depositDate;
if (data.depositAmount !== undefined) result.deposit_amount = data.depositAmount;
if (data.accountName !== undefined) result.account_name = data.accountName;
if (data.depositorName !== undefined) result.depositor_name = data.depositorName;
if (data.note !== undefined) result.note = data.note || null;
if (data.depositType !== undefined) result.deposit_type = data.depositType;
if (data.vendorId !== undefined) result.vendor_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
if (data.vendorName !== undefined) result.vendor_name = data.vendorName || null;
if (data.status !== undefined) result.status = data.status;
const result = await response.json();
return result;
if (!result.success || !result.data) {
return { success: false, error: result.message || '입금 내역 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 입금 등록 =====
export async function createDeposit(
data: Partial<DepositRecord>
): Promise<{ success: boolean; data?: DepositRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[DepositActions] POST deposit request:', apiData);
console.log('[DepositActions] POST deposit request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits`;
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[DepositActions] POST deposit response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '입금 등록에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[DepositActions] createDeposit error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[DepositActions] POST deposit response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '입금 등록에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 입금 수정 =====
@@ -349,42 +268,27 @@ export async function updateDeposit(
id: string,
data: Partial<DepositRecord>
): Promise<{ success: boolean; data?: DepositRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[DepositActions] PUT deposit request:', apiData);
console.log('[DepositActions] PUT deposit request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/deposits/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[DepositActions] PUT deposit response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '입금 수정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[DepositActions] updateDeposit error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[DepositActions] PUT deposit response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '입금 수정에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 거래처 목록 조회 =====
@@ -393,39 +297,30 @@ export async function getVendors(): Promise<{
data: { id: string; name: string }[];
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
return { success: false, data: [], error: `API 오류: ${response.status}` };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message };
}
const clients = result.data?.data || result.data || [];
return {
success: true,
data: clients.map((c: { id: number; name: string }) => ({
id: String(c.id),
name: c.name,
})),
};
} catch (error) {
console.error('[DepositActions] getVendors error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
if (error) {
return { success: false, data: [], error: error.message };
}
}
if (!response?.ok) {
return { success: false, data: [], error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message };
}
const clients = result.data?.data || result.data || [];
return {
success: true,
data: clients.map((c: { id: number; name: string }) => ({
id: String(c.id),
name: c.name,
})),
};
}

View File

@@ -1,21 +1,8 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { ExpectedExpenseRecord, TransactionType, PaymentStatus, ApprovalStatus } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface ExpectedExpenseApiData {
id: number;
@@ -126,7 +113,6 @@ export async function getExpectedExpenses(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -150,19 +136,24 @@ export async function getExpectedExpenses(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[ExpectedExpenseActions] GET expected-expenses error:', response.status);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 50, total: 0 },
error: `API 오류: ${response.status}`,
error: error.message,
};
}
if (!response?.ok) {
console.warn('[ExpectedExpenseActions] GET expected-expenses error:', response?.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 50, total: 0 },
error: `API 오류: ${response?.status}`,
};
}
@@ -214,22 +205,20 @@ export async function getExpectedExpenseById(id: string): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET' }
);
if (!response.ok) {
console.error('[ExpectedExpenseActions] GET expected-expense error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.error('[ExpectedExpenseActions] GET expected-expense error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -260,24 +249,26 @@ export async function createExpectedExpense(
data: Partial<ExpectedExpenseRecord>
): Promise<{ success: boolean; data?: ExpectedExpenseRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '미지급비용 등록에 실패했습니다.',
error: result?.message || '미지급비용 등록에 실패했습니다.',
};
}
@@ -300,24 +291,26 @@ export async function updateExpectedExpense(
data: Partial<ExpectedExpenseRecord>
): Promise<{ success: boolean; data?: ExpectedExpenseRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '미지급비용 수정에 실패했습니다.',
error: result?.message || '미지급비용 수정에 실패했습니다.',
};
}
@@ -337,22 +330,21 @@ export async function updateExpectedExpense(
// ===== 미지급비용 삭제 =====
export async function deleteExpectedExpense(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/${id}`,
{
method: 'DELETE',
headers,
}
{ method: 'DELETE' }
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '미지급비용 삭제에 실패했습니다.',
error: result?.message || '미지급비용 삭제에 실패했습니다.',
};
}
@@ -373,25 +365,26 @@ export async function deleteExpectedExpenses(ids: string[]): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses`,
{
method: 'DELETE',
headers,
body: JSON.stringify({
ids: ids.map(id => parseInt(id, 10)),
}),
}
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '미지급비용 일괄 삭제에 실패했습니다.',
error: result?.message || '미지급비용 일괄 삭제에 실패했습니다.',
};
}
@@ -418,13 +411,10 @@ export async function updateExpectedPaymentDate(
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/update-payment-date`,
{
method: 'PUT',
headers,
body: JSON.stringify({
ids: ids.map(id => parseInt(id, 10)),
expected_payment_date: expectedPaymentDate,
@@ -432,12 +422,16 @@ export async function updateExpectedPaymentDate(
}
);
const result = await response.json();
if (error) {
return { success: false, error: error.message };
}
if (!response.ok || !result.success) {
const result = await response?.json();
if (!response?.ok || !result.success) {
return {
success: false,
error: result.message || '예상 지급일 변경에 실패했습니다.',
error: result?.message || '예상 지급일 변경에 실패했습니다.',
};
}
@@ -465,7 +459,6 @@ export async function getExpectedExpenseSummary(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.startDate) searchParams.set('start_date', params.startDate);
@@ -477,17 +470,17 @@ export async function getExpectedExpenseSummary(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/expected-expenses/summary${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[ExpectedExpenseActions] GET summary error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.warn('[ExpectedExpenseActions] GET summary error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -520,19 +513,17 @@ export async function getClients(): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET' }
);
if (!response.ok) {
return { success: false, data: [], error: `API 오류: ${response.status}` };
if (error) {
return { success: false, data: [], error: error.message };
}
if (!response?.ok) {
return { success: false, data: [], error: `API 오류: ${response?.status}` };
}
const result = await response.json();
@@ -563,19 +554,17 @@ export async function getBankAccounts(): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?per_page=100`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET' }
);
if (!response.ok) {
return { success: false, data: [], error: `API 오류: ${response.status}` };
if (error) {
return { success: false, data: [], error: error.message };
}
if (!response?.ok) {
return { success: false, data: [], error: `API 오류: ${response?.status}` };
}
const result = await response.json();
@@ -599,4 +588,4 @@ export async function getBankAccounts(): Promise<{
console.error('[ExpectedExpenseActions] getBankAccounts error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}
}

View File

@@ -12,7 +12,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { PurchaseRecord, PurchaseType } from './types';
// ===== API 데이터 타입 =====
@@ -92,19 +92,6 @@ function transformFrontendToApi(data: Partial<PurchaseRecord>): Record<string, u
return result;
}
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
currentPage: number;
@@ -128,81 +115,75 @@ export async function getPurchases(params?: {
pagination: PaginationMeta;
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.clientId) searchParams.set('client_id', params.clientId);
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.search) searchParams.set('search', params.search);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.clientId) searchParams.set('client_id', params.clientId);
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.search) searchParams.set('search', params.search);
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases${queryString ? `?${queryString}` : ''}`;
console.log('[PurchaseActions] GET purchases:', url);
console.log('[PurchaseActions] GET purchases:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[PurchaseActions] GET purchases error:', response.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '매입 목록 조회에 실패했습니다.',
};
}
const paginatedData: PurchaseApiPaginatedResponse = result.data || {
data: [],
current_page: 1,
last_page: 1,
per_page: 20,
total: 0,
};
const purchases = (paginatedData.data || []).map(transformApiToFrontend);
return {
success: true,
data: purchases,
pagination: {
currentPage: paginatedData.current_page,
lastPage: paginatedData.last_page,
perPage: paginatedData.per_page,
total: paginatedData.total,
},
};
} catch (error) {
console.error('[PurchaseActions] getPurchases error:', error);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '서버 오류가 발생했습니다.',
error: error.message,
};
}
if (!response?.ok) {
console.warn('[PurchaseActions] GET purchases error:', response?.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response?.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '매입 목록 조회에 실패했습니다.',
};
}
const paginatedData: PurchaseApiPaginatedResponse = result.data || {
data: [],
current_page: 1,
last_page: 1,
per_page: 20,
total: 0,
};
const purchases = (paginatedData.data || []).map(transformApiToFrontend);
return {
success: true,
data: purchases,
pagination: {
currentPage: paginatedData.current_page,
lastPage: paginatedData.last_page,
perPage: paginatedData.per_page,
total: paginatedData.total,
},
};
}
// ===== 매입 상세 조회 =====
@@ -211,88 +192,52 @@ export async function getPurchaseById(id: string): Promise<{
data?: PurchaseRecord;
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.error('[PurchaseActions] GET purchase error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '매입 조회에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[PurchaseActions] getPurchaseById error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.error('[PurchaseActions] GET purchase error:', response?.status);
return { success: false, error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success || !result.data) {
return { success: false, error: result.message || '매입 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 매입 등록 =====
export async function createPurchase(
data: Partial<PurchaseRecord>
): Promise<{ success: boolean; data?: PurchaseRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[PurchaseActions] POST purchase request:', apiData);
console.log('[PurchaseActions] POST purchase request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases`;
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[PurchaseActions] POST purchase response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '매입 등록에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[PurchaseActions] createPurchase error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[PurchaseActions] POST purchase response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '매입 등록에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 매입 수정 =====
@@ -300,42 +245,27 @@ export async function updatePurchase(
id: string,
data: Partial<PurchaseRecord>
): Promise<{ success: boolean; data?: PurchaseRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[PurchaseActions] PUT purchase request:', apiData);
console.log('[PurchaseActions] PUT purchase request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[PurchaseActions] PUT purchase response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '매입 수정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[PurchaseActions] updatePurchase error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[PurchaseActions] PUT purchase response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '매입 수정에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 세금계산서 수취 상태 토글 =====
@@ -348,35 +278,21 @@ export async function togglePurchaseTaxInvoice(
// ===== 매입 삭제 =====
export async function deletePurchase(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`;
const { response, error } = await serverFetch(url, { method: 'DELETE' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}`,
{
method: 'DELETE',
headers,
}
);
const result = await response.json();
console.log('[PurchaseActions] DELETE purchase response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '매입 삭제에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[PurchaseActions] deletePurchase error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[PurchaseActions] DELETE purchase response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '매입 삭제에 실패했습니다.' };
}
return { success: true };
}
// ===== 매입 확정 =====
@@ -385,38 +301,21 @@ export async function confirmPurchase(id: string): Promise<{
data?: PurchaseRecord;
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}/confirm`;
const { response, error } = await serverFetch(url, { method: 'PUT' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/purchases/${id}/confirm`,
{
method: 'PUT',
headers,
}
);
const result = await response.json();
console.log('[PurchaseActions] PUT confirm response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '매입 확정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[PurchaseActions] confirmPurchase error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[PurchaseActions] PUT confirm response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '매입 확정에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 은행 계좌 목록 조회 =====
@@ -425,43 +324,34 @@ export async function getBankAccounts(): Promise<{
data: { id: string; bankName: string; accountName: string; accountNumber: string }[];
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?per_page=100`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?per_page=100`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
return { success: false, data: [], error: `API 오류: ${response.status}` };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message };
}
const accounts = result.data?.data || result.data || [];
return {
success: true,
data: accounts.map((a: { id: number; bank_name: string; account_name: string; account_number: string }) => ({
id: String(a.id),
bankName: a.bank_name,
accountName: a.account_name,
accountNumber: a.account_number,
})),
};
} catch (error) {
console.error('[PurchaseActions] getBankAccounts error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
if (error) {
return { success: false, data: [], error: error.message };
}
if (!response?.ok) {
return { success: false, data: [], error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message };
}
const accounts = result.data?.data || result.data || [];
return {
success: true,
data: accounts.map((a: { id: number; bank_name: string; account_name: string; account_number: string }) => ({
id: String(a.id),
bankName: a.bank_name,
accountName: a.account_name,
accountNumber: a.account_number,
})),
};
}
// ===== 거래처 목록 조회 =====
@@ -470,39 +360,30 @@ export async function getVendors(): Promise<{
data: { id: string; name: string }[];
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
return { success: false, data: [], error: `API 오류: ${response.status}` };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message };
}
const clients = result.data?.data || result.data || [];
return {
success: true,
data: clients.map((c: { id: number; name: string }) => ({
id: String(c.id),
name: c.name,
})),
};
} catch (error) {
console.error('[PurchaseActions] getVendors error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
if (error) {
return { success: false, data: [], error: error.message };
}
if (!response?.ok) {
return { success: false, data: [], error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message };
}
const clients = result.data?.data || result.data || [];
return {
success: true,
data: clients.map((c: { id: number; name: string }) => ({
id: String(c.id),
name: c.name,
})),
};
}

View File

@@ -1,21 +1,9 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { VendorReceivables, CategoryType, MonthlyAmount } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface CategoryAmountApi {
category: CategoryType;
@@ -65,7 +53,6 @@ export async function getReceivablesList(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.year) searchParams.set('year', String(params.year));
@@ -77,18 +64,18 @@ export async function getReceivablesList(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivables${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[ReceivablesActions] GET receivables error:', response.status);
if (error) {
return { success: false, data: [], error: error.message };
}
if (!response?.ok) {
console.warn('[ReceivablesActions] GET receivables error:', response?.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -134,7 +121,6 @@ export async function getReceivablesSummary(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.year) searchParams.set('year', String(params.year));
@@ -142,17 +128,17 @@ export async function getReceivablesSummary(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivables/summary${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[ReceivablesActions] GET summary error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.warn('[ReceivablesActions] GET summary error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -196,12 +182,10 @@ export async function updateOverdueStatus(
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivables/overdue-status`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'PUT',
headers,
body: JSON.stringify({
updates: updates.map(item => ({
id: item.id,
@@ -210,11 +194,15 @@ export async function updateOverdueStatus(
}),
});
if (!response.ok) {
console.warn('[ReceivablesActions] PUT overdue-status error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.warn('[ReceivablesActions] PUT overdue-status error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -296,4 +284,4 @@ export async function exportReceivablesExcel(params?: {
error: '서버 오류가 발생했습니다.',
};
}
}
}

View File

@@ -13,7 +13,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
SalesRecord,
SaleApiData,
@@ -21,19 +21,6 @@ import type {
} from './types';
import { transformApiToFrontend, transformFrontendToApi } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
currentPage: number;
@@ -57,82 +44,76 @@ export async function getSales(params?: {
pagination: PaginationMeta;
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.clientId) searchParams.set('client_id', params.clientId);
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.search) searchParams.set('search', params.search);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.clientId) searchParams.set('client_id', params.clientId);
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.search) searchParams.set('search', params.search);
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales${queryString ? `?${queryString}` : ''}`;
console.log('[SalesActions] GET sales:', url);
console.log('[SalesActions] GET sales:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[SalesActions] GET sales error:', response.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '매출 목록 조회에 실패했습니다.',
};
}
// API 응답 구조: { success, data: { data: [], current_page, last_page, per_page, total } }
const paginatedData: SaleApiPaginatedResponse = result.data || {
data: [],
current_page: 1,
last_page: 1,
per_page: 20,
total: 0,
};
const sales = (paginatedData.data || []).map(transformApiToFrontend);
return {
success: true,
data: sales,
pagination: {
currentPage: paginatedData.current_page,
lastPage: paginatedData.last_page,
perPage: paginatedData.per_page,
total: paginatedData.total,
},
};
} catch (error) {
console.error('[SalesActions] getSales error:', error);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '서버 오류가 발생했습니다.',
error: error.message,
};
}
if (!response?.ok) {
console.warn('[SalesActions] GET sales error:', response?.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response?.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '매출 목록 조회에 실패했습니다.',
};
}
// API 응답 구조: { success, data: { data: [], current_page, last_page, per_page, total } }
const paginatedData: SaleApiPaginatedResponse = result.data || {
data: [],
current_page: 1,
last_page: 1,
per_page: 20,
total: 0,
};
const sales = (paginatedData.data || []).map(transformApiToFrontend);
return {
success: true,
data: sales,
pagination: {
currentPage: paginatedData.current_page,
lastPage: paginatedData.last_page,
perPage: paginatedData.per_page,
total: paginatedData.total,
},
};
}
// ===== 매출 상세 조회 =====
@@ -141,88 +122,52 @@ export async function getSaleById(id: string): Promise<{
data?: SalesRecord;
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.error('[SalesActions] GET sale error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '매출 조회에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[SalesActions] getSaleById error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.error('[SalesActions] GET sale error:', response?.status);
return { success: false, error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success || !result.data) {
return { success: false, error: result.message || '매출 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 매출 등록 =====
export async function createSale(
data: Partial<SalesRecord>
): Promise<{ success: boolean; data?: SalesRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[SalesActions] POST sale request:', apiData);
console.log('[SalesActions] POST sale request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales`;
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[SalesActions] POST sale response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '매출 등록에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[SalesActions] createSale error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[SalesActions] POST sale response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '매출 등록에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 매출 수정 =====
@@ -230,42 +175,27 @@ export async function updateSale(
id: string,
data: Partial<SalesRecord>
): Promise<{ success: boolean; data?: SalesRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[SalesActions] PUT sale request:', apiData);
console.log('[SalesActions] PUT sale request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[SalesActions] PUT sale response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '매출 수정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[SalesActions] updateSale error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[SalesActions] PUT sale response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '매출 수정에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 세금계산서/거래명세서 발행 상태 토글 =====
@@ -284,35 +214,21 @@ export async function toggleSaleIssuance(
// ===== 매출 삭제 =====
export async function deleteSale(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`;
const { response, error } = await serverFetch(url, { method: 'DELETE' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}`,
{
method: 'DELETE',
headers,
}
);
const result = await response.json();
console.log('[SalesActions] DELETE sale response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '매출 삭제에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[SalesActions] deleteSale error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[SalesActions] DELETE sale response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '매출 삭제에 실패했습니다.' };
}
return { success: true };
}
// ===== 매출 확정 =====
@@ -321,38 +237,21 @@ export async function confirmSale(id: string): Promise<{
data?: SalesRecord;
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}/confirm`;
const { response, error } = await serverFetch(url, { method: 'PUT' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/${id}/confirm`,
{
method: 'PUT',
headers,
}
);
const result = await response.json();
console.log('[SalesActions] PUT confirm response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '매출 확정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[SalesActions] confirmSale error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[SalesActions] PUT confirm response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '매출 확정에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 매출 요약 통계 =====
@@ -371,58 +270,43 @@ export async function getSalesSummary(params?: {
};
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/summary${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/sales/summary${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[SalesActions] GET summary error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
error: result.message || '매출 요약 조회에 실패했습니다.',
};
}
// API 응답 → 프론트엔드 형식 변환
const summary = result.data || {};
return {
success: true,
data: {
totalAmount: parseFloat(summary.total_amount) || 0,
totalCount: summary.total_count || 0,
confirmedAmount: parseFloat(summary.confirmed_amount) || 0,
confirmedCount: summary.confirmed_count || 0,
draftAmount: parseFloat(summary.draft_amount) || 0,
draftCount: summary.draft_count || 0,
},
};
} catch (error) {
console.error('[SalesActions] getSalesSummary error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.warn('[SalesActions] GET summary error:', response?.status);
return { success: false, error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success) {
return { success: false, error: result.message || '매출 요약 조회에 실패했습니다.' };
}
// API 응답 → 프론트엔드 형식 변환
const summary = result.data || {};
return {
success: true,
data: {
totalAmount: parseFloat(summary.total_amount) || 0,
totalCount: summary.total_count || 0,
confirmedAmount: parseFloat(summary.confirmed_amount) || 0,
confirmedCount: summary.confirmed_count || 0,
draftAmount: parseFloat(summary.draft_amount) || 0,
draftCount: summary.draft_count || 0,
},
};
}

View File

@@ -1,21 +1,9 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { VendorLedgerItem, VendorLedgerDetail, VendorLedgerSummary, TransactionEntry } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface VendorLedgerApiItem {
id: number;
@@ -171,7 +159,6 @@ export async function getVendorLedgerList(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -185,19 +172,24 @@ export async function getVendorLedgerList(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/vendor-ledger${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[VendorLedgerActions] GET vendor-ledger error:', response.status);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
error: error.message,
};
}
if (!response?.ok) {
console.warn('[VendorLedgerActions] GET vendor-ledger error:', response?.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response?.status}`,
};
}
@@ -252,7 +244,6 @@ export async function getVendorLedgerSummary(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.startDate) searchParams.set('start_date', params.startDate);
@@ -261,17 +252,17 @@ export async function getVendorLedgerSummary(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/vendor-ledger/summary${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[VendorLedgerActions] GET summary error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.warn('[VendorLedgerActions] GET summary error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -315,7 +306,6 @@ export async function getVendorLedgerDetail(clientId: string, params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.startDate) searchParams.set('start_date', params.startDate);
@@ -324,17 +314,17 @@ export async function getVendorLedgerDetail(clientId: string, params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/vendor-ledger/${clientId}${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.error('[VendorLedgerActions] GET detail error:', response.status);
if (error) {
return { success: false, error: error.message };
}
if (!response?.ok) {
console.error('[VendorLedgerActions] GET detail error:', response?.status);
return {
success: false,
error: `API 오류: ${response.status}`,
error: `API 오류: ${response?.status}`,
};
}
@@ -484,4 +474,4 @@ export async function exportVendorLedgerDetailPdf(clientId: string, params?: {
error: '서버 오류가 발생했습니다.',
};
}
}
}

View File

@@ -12,7 +12,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
Vendor,
ClientApiData,
@@ -24,21 +24,6 @@ import type {
BadDebtStatus,
} from './types';
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* API 데이터 → 프론트엔드 타입 변환
*/
@@ -166,82 +151,59 @@ export async function getClients(params?: {
q?: string;
only_active?: boolean;
}): Promise<{ success: boolean; data: Vendor[]; total: number; error?: string }> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('size', String(params.size));
if (params?.q) searchParams.set('q', params.q);
if (params?.only_active !== undefined) searchParams.set('only_active', String(params.only_active));
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('size', String(params.size));
if (params?.q) searchParams.set('q', params.q);
if (params?.only_active !== undefined) searchParams.set('only_active', String(params.only_active));
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?${searchParams.toString()}`;
console.log('[VendorActions] GET clients:', url);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?${searchParams.toString()}`;
console.log('[VendorActions] GET clients:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.error('[VendorActions] GET clients error:', response.status);
return { success: false, data: [], total: 0, error: `HTTP ${response.status}` };
}
const result: ApiResponse<PaginatedResponse<ClientApiData>> = await response.json();
console.log('[VendorActions] GET clients response:', result.success, result.data?.total);
if (!result.success || !result.data) {
return { success: false, data: [], total: 0, error: result.message };
}
const vendors = result.data.data.map(transformApiToFrontend);
return {
success: true,
data: vendors,
total: result.data.total,
};
} catch (error) {
console.error('[VendorActions] getClients error:', error);
return { success: false, data: [], total: 0, error: '서버 오류가 발생했습니다.' };
if (error) {
return { success: false, data: [], total: 0, error: error.message };
}
if (!response?.ok) {
console.error('[VendorActions] GET clients error:', response?.status);
return { success: false, data: [], total: 0, error: `HTTP ${response?.status}` };
}
const result: ApiResponse<PaginatedResponse<ClientApiData>> = await response.json();
console.log('[VendorActions] GET clients response:', result.success, result.data?.total);
if (!result.success || !result.data) {
return { success: false, data: [], total: 0, error: result.message };
}
const vendors = result.data.data.map(transformApiToFrontend);
return { success: true, data: vendors, total: result.data.total };
}
/**
* 거래처 상세 조회
*/
export async function getClientById(id: string): Promise<Vendor | null> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.error('[VendorActions] GET client error:', response.status);
return null;
}
const result: ApiResponse<ClientApiData> = await response.json();
console.log('[VendorActions] GET client response:', result.success);
if (!result.success || !result.data) {
return null;
}
return transformApiToFrontend(result.data);
} catch (error) {
console.error('[VendorActions] getClientById error:', error);
if (error || !response?.ok) {
console.error('[VendorActions] GET client error:', error?.message || response?.status);
return null;
}
const result: ApiResponse<ClientApiData> = await response.json();
console.log('[VendorActions] GET client response:', result.success);
if (!result.success || !result.data) {
return null;
}
return transformApiToFrontend(result.data);
}
/**
@@ -250,42 +212,27 @@ export async function getClientById(id: string): Promise<Vendor | null> {
export async function createClient(
data: Partial<Vendor>
): Promise<{ success: boolean; data?: Vendor; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[VendorActions] POST client request:', apiData);
console.log('[VendorActions] POST client request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients`;
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[VendorActions] POST client response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '거래처 등록에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[VendorActions] createClient error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[VendorActions] POST client response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '거래처 등록에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
/**
@@ -295,113 +242,67 @@ export async function updateClient(
id: string,
data: Partial<Vendor>
): Promise<{ success: boolean; data?: Vendor; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[VendorActions] PUT client request:', apiData);
console.log('[VendorActions] PUT client request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[VendorActions] PUT client response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '거래처 수정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[VendorActions] updateClient error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[VendorActions] PUT client response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '거래처 수정에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
/**
* 거래처 삭제
*/
export async function deleteClient(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`;
const { response, error } = await serverFetch(url, { method: 'DELETE' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}`,
{
method: 'DELETE',
headers,
}
);
const result = await response.json();
console.log('[VendorActions] DELETE client response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '거래처 삭제에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[VendorActions] deleteClient error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[VendorActions] DELETE client response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '거래처 삭제에 실패했습니다.' };
}
return { success: true };
}
/**
* 거래처 활성/비활성 토글
*/
export async function toggleClientActive(id: string): Promise<{ success: boolean; data?: Vendor; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}/toggle`;
const { response, error } = await serverFetch(url, { method: 'PATCH' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients/${id}/toggle`,
{
method: 'PATCH',
headers,
}
);
const result = await response.json();
console.log('[VendorActions] PATCH toggle response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '상태 변경에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[VendorActions] toggleClientActive error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[VendorActions] PATCH toggle response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '상태 변경에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}

View File

@@ -1,21 +1,8 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { WithdrawalRecord, WithdrawalType } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface WithdrawalApiData {
id: number;
@@ -58,6 +45,22 @@ function transformApiToFrontend(apiData: WithdrawalApiData): WithdrawalRecord {
};
}
// ===== Frontend → API 변환 =====
function transformFrontendToApi(data: Partial<WithdrawalRecord>): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (data.withdrawalDate !== undefined) result.withdrawal_date = data.withdrawalDate;
if (data.withdrawalAmount !== undefined) result.withdrawal_amount = data.withdrawalAmount;
if (data.accountName !== undefined) result.account_name = data.accountName;
if (data.recipientName !== undefined) result.recipient_name = data.recipientName;
if (data.note !== undefined) result.note = data.note || null;
if (data.withdrawalType !== undefined) result.withdrawal_type = data.withdrawalType;
if (data.vendorId !== undefined) result.vendor_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
if (data.vendorName !== undefined) result.vendor_name = data.vendorName || null;
return result;
}
// ===== 출금 내역 조회 =====
export async function getWithdrawals(params?: {
page?: number;
@@ -78,111 +81,91 @@ export async function getWithdrawals(params?: {
};
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.withdrawalType && params.withdrawalType !== 'all') {
searchParams.set('withdrawal_type', params.withdrawalType);
}
if (params?.vendor && params.vendor !== 'all') {
searchParams.set('vendor', params.vendor);
}
if (params?.search) searchParams.set('search', params.search);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.perPage) searchParams.set('per_page', String(params.perPage));
if (params?.startDate) searchParams.set('start_date', params.startDate);
if (params?.endDate) searchParams.set('end_date', params.endDate);
if (params?.withdrawalType && params.withdrawalType !== 'all') {
searchParams.set('withdrawal_type', params.withdrawalType);
}
if (params?.vendor && params.vendor !== 'all') {
searchParams.set('vendor', params.vendor);
}
if (params?.search) searchParams.set('search', params.search);
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals${queryString ? `?${queryString}` : ''}`;
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.warn('[WithdrawalActions] GET withdrawals error:', response.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '출금 내역 조회에 실패했습니다.',
};
}
const withdrawals = (result.data || []).map(transformApiToFrontend);
const meta: PaginationMeta = result.meta || {
current_page: 1,
last_page: 1,
per_page: 20,
total: withdrawals.length,
};
return {
success: true,
data: withdrawals,
pagination: {
currentPage: meta.current_page,
lastPage: meta.last_page,
perPage: meta.per_page,
total: meta.total,
},
};
} catch (error) {
console.error('[WithdrawalActions] getWithdrawals error:', error);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '서버 오류가 발생했습니다.',
error: error.message,
};
}
if (!response?.ok) {
console.warn('[WithdrawalActions] GET withdrawals error:', response?.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response?.status}`,
};
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: result.message || '출금 내역 조회에 실패했습니다.',
};
}
const withdrawals = (result.data || []).map(transformApiToFrontend);
const meta: PaginationMeta = result.meta || {
current_page: 1,
last_page: 1,
per_page: 20,
total: withdrawals.length,
};
return {
success: true,
data: withdrawals,
pagination: {
currentPage: meta.current_page,
lastPage: meta.last_page,
perPage: meta.per_page,
total: meta.total,
},
};
}
// ===== 출금 내역 삭제 =====
export async function deleteWithdrawal(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`;
const { response, error } = await serverFetch(url, { method: 'DELETE' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`,
{
method: 'DELETE',
headers,
}
);
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '출금 내역 삭제에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[WithdrawalActions] deleteWithdrawal error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '출금 내역 삭제에 실패했습니다.' };
}
return { success: true };
}
// ===== 계정과목명 일괄 저장 =====
@@ -190,38 +173,26 @@ export async function updateWithdrawalTypes(
ids: string[],
withdrawalType: string
): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/bulk-update-type`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify({
ids: ids.map(id => parseInt(id, 10)),
withdrawal_type: withdrawalType,
}),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/bulk-update-type`,
{
method: 'PUT',
headers,
body: JSON.stringify({
ids: ids.map(id => parseInt(id, 10)),
withdrawal_type: withdrawalType,
}),
}
);
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '계정과목명 저장에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[WithdrawalActions] updateWithdrawalTypes error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '계정과목명 저장에 실패했습니다.' };
}
return { success: true };
}
// ===== 출금 상세 조회 =====
@@ -230,104 +201,52 @@ export async function getWithdrawalById(id: string): Promise<{
data?: WithdrawalRecord;
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.error('[WithdrawalActions] GET withdrawal error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '출금 내역 조회에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WithdrawalActions] getWithdrawalById error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
}
// ===== Frontend → API 변환 =====
function transformFrontendToApi(data: Partial<WithdrawalRecord>): Record<string, unknown> {
const result: Record<string, unknown> = {};
if (!response?.ok) {
console.error('[WithdrawalActions] GET withdrawal error:', response?.status);
return { success: false, error: `API 오류: ${response?.status}` };
}
if (data.withdrawalDate !== undefined) result.withdrawal_date = data.withdrawalDate;
if (data.withdrawalAmount !== undefined) result.withdrawal_amount = data.withdrawalAmount;
if (data.accountName !== undefined) result.account_name = data.accountName;
if (data.recipientName !== undefined) result.recipient_name = data.recipientName;
if (data.note !== undefined) result.note = data.note || null;
if (data.withdrawalType !== undefined) result.withdrawal_type = data.withdrawalType;
if (data.vendorId !== undefined) result.vendor_id = data.vendorId ? parseInt(data.vendorId, 10) : null;
if (data.vendorName !== undefined) result.vendor_name = data.vendorName || null;
const result = await response.json();
return result;
if (!result.success || !result.data) {
return { success: false, error: result.message || '출금 내역 조회에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 출금 등록 =====
export async function createWithdrawal(
data: Partial<WithdrawalRecord>
): Promise<{ success: boolean; data?: WithdrawalRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[WithdrawalActions] POST withdrawal request:', apiData);
console.log('[WithdrawalActions] POST withdrawal request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals`;
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[WithdrawalActions] POST withdrawal response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '출금 등록에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WithdrawalActions] createWithdrawal error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[WithdrawalActions] POST withdrawal response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '출금 등록에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 출금 수정 =====
@@ -335,42 +254,27 @@ export async function updateWithdrawal(
id: string,
data: Partial<WithdrawalRecord>
): Promise<{ success: boolean; data?: WithdrawalRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[WithdrawalActions] PUT withdrawal request:', apiData);
console.log('[WithdrawalActions] PUT withdrawal request:', apiData);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/withdrawals/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
const result = await response.json();
console.log('[WithdrawalActions] PUT withdrawal response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '출금 수정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[WithdrawalActions] updateWithdrawal error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
if (error) {
return { success: false, error: error.message };
}
const result = await response?.json();
console.log('[WithdrawalActions] PUT withdrawal response:', result);
if (!response?.ok || !result.success) {
return { success: false, error: result?.message || '출금 수정에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
}
// ===== 거래처 목록 조회 =====
@@ -379,39 +283,30 @@ export async function getVendors(): Promise<{
data: { id: string; name: string }[];
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/clients?per_page=100`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
return { success: false, data: [], error: `API 오류: ${response.status}` };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message };
}
const clients = result.data?.data || result.data || [];
return {
success: true,
data: clients.map((c: { id: number; name: string }) => ({
id: String(c.id),
name: c.name,
})),
};
} catch (error) {
console.error('[WithdrawalActions] getVendors error:', error);
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
if (error) {
return { success: false, data: [], error: error.message };
}
}
if (!response?.ok) {
return { success: false, data: [], error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success) {
return { success: false, data: [], error: result.message };
}
const clients = result.data?.data || result.data || [];
return {
success: true,
data: clients.map((c: { id: number; name: string }) => ({
id: String(c.id),
name: c.name,
})),
};
}

View File

@@ -10,7 +10,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { ApprovalRecord, ApprovalType, ApprovalStatus } from './types';
// ============================================
@@ -81,21 +81,6 @@ interface InboxStepApiData {
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* API 상태 → 프론트엔드 상태 변환
*/
@@ -108,6 +93,22 @@ function mapApiStatus(apiStatus: string): ApprovalStatus {
return statusMap[apiStatus] || 'pending';
}
/**
* 프론트엔드 탭 상태 → 백엔드 API 상태 변환
* 백엔드 inbox API가 기대하는 값:
* - requested: 결재 요청 (현재 내 차례) = 미결재
* - completed: 내가 처리 완료 = 결재완료
* - rejected: 내가 반려한 문서 = 결재반려
*/
function mapTabToApiStatus(tabStatus: string): string | undefined {
const statusMap: Record<string, string> = {
'pending': 'requested', // 미결재 → 결재 요청
'approved': 'completed', // 결재완료 → 처리 완료
'rejected': 'rejected', // 반려 (동일)
};
return statusMap[tabStatus];
}
/**
* 양식 카테고리 → 결재 유형 변환
*/
@@ -175,16 +176,19 @@ export async function getInbox(params?: {
approval_type?: string;
sort_by?: string;
sort_dir?: 'asc' | 'desc';
}): Promise<{ data: ApprovalRecord[]; total: number; lastPage: number }> {
}): Promise<{ data: ApprovalRecord[]; total: number; lastPage: number; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
if (params?.search) searchParams.set('search', params.search);
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
// 프론트엔드 탭 상태를 백엔드 API 상태로 변환
const apiStatus = mapTabToApiStatus(params.status);
if (apiStatus) {
searchParams.set('status', apiStatus);
}
}
if (params?.approval_type && params.approval_type !== 'all') {
searchParams.set('approval_type', params.approval_type);
@@ -194,20 +198,20 @@ export async function getInbox(params?: {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/inbox?${searchParams.toString()}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.error('[ApprovalBoxActions] GET inbox error:', response.status);
if (error?.__authError) {
return { data: [], total: 0, lastPage: 1, __authError: true };
}
if (!response) {
console.error('[ApprovalBoxActions] GET inbox error:', error?.message);
return { data: [], total: 0, lastPage: 1 };
}
const result: ApiResponse<PaginatedResponse<InboxApiData>> = await response.json();
if (!result.success || !result.data?.data) {
if (!response.ok || !result.success || !result.data?.data) {
console.warn('[ApprovalBoxActions] No data in response');
return { data: [], total: 0, lastPage: 1 };
}
@@ -228,25 +232,19 @@ export async function getInbox(params?: {
*/
export async function getInboxSummary(): Promise<InboxSummary | null> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/inbox/summary`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET' }
);
if (!response.ok) {
console.error('[ApprovalBoxActions] GET inbox/summary error:', response.status);
if (error?.__authError || !response) {
console.error('[ApprovalBoxActions] GET inbox/summary error:', error?.message);
return null;
}
const result: ApiResponse<InboxSummary> = await response.json();
if (!result.success || !result.data) {
if (!response.ok || !result.success || !result.data) {
return null;
}
@@ -260,19 +258,24 @@ export async function getInboxSummary(): Promise<InboxSummary | null> {
/**
* 승인 처리
*/
export async function approveDocument(id: string, comment?: string): Promise<{ success: boolean; error?: string }> {
export async function approveDocument(id: string, comment?: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/approve`,
{
method: 'POST',
headers,
body: JSON.stringify({ comment: comment || '' }),
}
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || '승인 처리에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -295,7 +298,7 @@ export async function approveDocument(id: string, comment?: string): Promise<{ s
/**
* 반려 처리
*/
export async function rejectDocument(id: string, comment: string): Promise<{ success: boolean; error?: string }> {
export async function rejectDocument(id: string, comment: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
if (!comment?.trim()) {
return {
@@ -304,17 +307,22 @@ export async function rejectDocument(id: string, comment: string): Promise<{ suc
};
}
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/reject`,
{
method: 'POST',
headers,
body: JSON.stringify({ comment }),
}
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || '반려 처리에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {

View File

@@ -68,24 +68,23 @@ export function ApprovalLineSection({ data, onChange }: ApprovalLineSectionProps
</div>
) : (
data.map((person, index) => (
<div key={person.id} className="flex items-center gap-2">
<div key={`${person.id}-${index}`} className="flex items-center gap-2">
<span className="w-8 text-center text-sm text-gray-500">{index + 1}</span>
<Select
value={person.id.startsWith('temp-') ? '' : person.id}
value={person.id.startsWith('temp-') ? undefined : person.id}
onValueChange={(value) => handleChange(index, value)}
>
<SelectTrigger className="flex-1">
{/* 이미 선택된 값이 있으면 직접 표시, 없으면 placeholder */}
{person.name && !person.id.startsWith('temp-') ? (
<span>{person.department} / {person.position} / {person.name}</span>
) : (
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼" />
)}
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼">
{person.name && !person.id.startsWith('temp-')
? `${person.department || ''} / ${person.position || ''} / ${person.name}`
: null}
</SelectValue>
</SelectTrigger>
<SelectContent>
{employees.map((employee) => (
<SelectItem key={employee.id} value={employee.id}>
{employee.department} / {employee.position} / {employee.name}
{employee.department || ''} / {employee.position || ''} / {employee.name}
</SelectItem>
))}
</SelectContent>

View File

@@ -68,24 +68,23 @@ export function ReferenceSection({ data, onChange }: ReferenceSectionProps) {
</div>
) : (
data.map((person, index) => (
<div key={person.id} className="flex items-center gap-2">
<div key={`${person.id}-${index}`} className="flex items-center gap-2">
<span className="w-8 text-center text-sm text-gray-500">{index + 1}</span>
<Select
value={person.id.startsWith('temp-') ? '' : person.id}
value={person.id.startsWith('temp-') ? undefined : person.id}
onValueChange={(value) => handleChange(index, value)}
>
<SelectTrigger className="flex-1">
{/* 이미 선택된 값이 있으면 직접 표시, 없으면 placeholder */}
{person.name && !person.id.startsWith('temp-') ? (
<span>{person.department} / {person.position} / {person.name}</span>
) : (
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼" />
)}
<SelectValue placeholder="부서명 / 직책명 / 이름 ▼">
{person.name && !person.id.startsWith('temp-')
? `${person.department || ''} / ${person.position || ''} / ${person.name}`
: null}
</SelectValue>
</SelectTrigger>
<SelectContent>
{employees.map((employee) => (
<SelectItem key={employee.id} value={employee.id}>
{employee.department} / {employee.position} / {employee.name}
{employee.department || ''} / {employee.position || ''} / {employee.name}
</SelectItem>
))}
</SelectContent>

View File

@@ -11,6 +11,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
ExpenseEstimateItem,
ApprovalPerson,
@@ -74,21 +75,6 @@ interface ApprovalCreateResponse {
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* 비용견적서 API 데이터 → 프론트엔드 데이터 변환
*/
@@ -141,6 +127,8 @@ function transformEmployee(employee: EmployeeApiData): ApprovalPerson {
* 파일 업로드
* @param files 업로드할 파일 배열
* @returns 업로드된 파일 정보 배열
*
* NOTE: 파일 업로드는 multipart/form-data가 필요하므로 serverFetch 대신 직접 fetch 사용
*/
export async function uploadFiles(files: File[]): Promise<{
success: boolean;
@@ -210,7 +198,6 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
finalDifference: number;
} | null> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (yearMonth) {
@@ -219,12 +206,15 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/reports/expense-estimate?${searchParams.toString()}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error || !response) {
console.error('[DocumentCreateActions] GET expense-estimate error:', error?.message);
return null;
}
if (!response.ok) {
console.error('[DocumentCreateActions] GET expense-estimate error:', response.status);
return null;
@@ -254,7 +244,6 @@ export async function getExpenseEstimateItems(yearMonth?: string): Promise<{
*/
export async function getEmployees(search?: string): Promise<ApprovalPerson[]> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
searchParams.set('per_page', '100');
if (search) {
@@ -263,12 +252,15 @@ export async function getEmployees(search?: string): Promise<ApprovalPerson[]> {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees?${searchParams.toString()}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error || !response) {
console.error('[DocumentCreateActions] GET employees error:', error?.message);
return [];
}
if (!response.ok) {
console.error('[DocumentCreateActions] GET employees error:', response.status);
return [];
@@ -296,8 +288,6 @@ export async function createApproval(formData: DocumentFormData): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
// 새 첨부파일 업로드
const newFiles = formData.proposalData?.attachments
|| formData.expenseReportData?.attachments
@@ -332,15 +322,21 @@ export async function createApproval(formData: DocumentFormData): Promise<{
content: getDocumentContent(formData, uploadedFiles),
};
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals`,
{
method: 'POST',
headers,
body: JSON.stringify(requestBody),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '문서 저장에 실패했습니다.',
};
}
const result: ApiResponse<ApprovalCreateResponse> = await response.json();
if (!response.ok || !result.success) {
@@ -374,17 +370,21 @@ export async function submitApproval(id: number): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/submit`,
{
method: 'POST',
headers,
body: JSON.stringify({}),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '문서 상신에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -450,17 +450,17 @@ export async function getApprovalById(id: number): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error || !response) {
return { success: false, error: error?.message || '문서 조회에 실패했습니다.' };
}
if (!response.ok) {
if (response.status === 404) {
return { success: false, error: '문서를 찾을 수 없습니다.' };
@@ -476,9 +476,9 @@ export async function getApprovalById(id: number): Promise<{
// API 응답을 프론트엔드 형식으로 변환
const apiData = result.data;
const formData = transformApiToFormData(apiData);
const formDataResult = transformApiToFormData(apiData);
return { success: true, data: formData };
return { success: true, data: formDataResult };
} catch (error) {
console.error('[DocumentCreateActions] getApprovalById error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
@@ -494,8 +494,6 @@ export async function updateApproval(id: number, formData: DocumentFormData): Pr
error?: string;
}> {
try {
const headers = await getApiHeaders();
// 새 첨부파일 업로드
const newFiles = formData.proposalData?.attachments
|| formData.expenseReportData?.attachments
@@ -528,15 +526,21 @@ export async function updateApproval(id: number, formData: DocumentFormData): Pr
content: getDocumentContent(formData, uploadedFiles),
};
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
{
method: 'PATCH',
headers,
body: JSON.stringify(requestBody),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '문서 수정에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -601,16 +605,20 @@ export async function deleteApproval(id: number): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
{
method: 'DELETE',
headers,
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '문서 삭제에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {

View File

@@ -12,7 +12,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { DraftRecord, DocumentStatus, Approver } from './types';
// ============================================
@@ -85,21 +85,6 @@ interface ApprovalStepApiData {
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* API 상태 → 프론트엔드 상태 변환
*/
@@ -193,9 +178,8 @@ export async function getDrafts(params?: {
status?: string;
sort_by?: string;
sort_dir?: 'asc' | 'desc';
}): Promise<{ data: DraftRecord[]; total: number; lastPage: number }> {
}): Promise<{ data: DraftRecord[]; total: number; lastPage: number; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -217,27 +201,20 @@ export async function getDrafts(params?: {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/drafts?${searchParams.toString()}`;
console.log('[DraftBoxActions] Fetching:', url);
console.log('[DraftBoxActions] Headers:', JSON.stringify(headers, null, 2));
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error?.__authError) {
return { data: [], total: 0, lastPage: 1, __authError: true };
}
console.log('[DraftBoxActions] Response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('[DraftBoxActions] GET drafts error:', response.status, errorText);
if (!response) {
console.error('[DraftBoxActions] GET drafts error:', error?.message);
return { data: [], total: 0, lastPage: 1 };
}
const result: ApiResponse<PaginatedResponse<ApprovalApiData>> = await response.json();
console.log('[DraftBoxActions] Result:', JSON.stringify(result, null, 2).slice(0, 500));
if (!result.success || !result.data?.data) {
if (!response.ok || !result.success || !result.data?.data) {
console.warn('[DraftBoxActions] No data in response');
return { data: [], total: 0, lastPage: 1 };
}
@@ -258,25 +235,19 @@ export async function getDrafts(params?: {
*/
export async function getDraftsSummary(): Promise<DraftsSummary | null> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/drafts/summary`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET' }
);
if (!response.ok) {
console.error('[DraftBoxActions] GET summary error:', response.status);
if (error?.__authError || !response) {
console.error('[DraftBoxActions] GET summary error:', error?.message);
return null;
}
const result: ApiResponse<DraftsSummary> = await response.json();
if (!result.success || !result.data) {
if (!response.ok || !result.success || !result.data) {
return null;
}
@@ -292,25 +263,19 @@ export async function getDraftsSummary(): Promise<DraftsSummary | null> {
*/
export async function getDraftById(id: string): Promise<DraftRecord | null> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET' }
);
if (!response.ok) {
console.error('[DraftBoxActions] GET draft error:', response.status);
if (error?.__authError || !response) {
console.error('[DraftBoxActions] GET draft error:', error?.message);
return null;
}
const result: ApiResponse<ApprovalApiData> = await response.json();
if (!result.success || !result.data) {
if (!response.ok || !result.success || !result.data) {
return null;
}
@@ -324,18 +289,21 @@ export async function getDraftById(id: string): Promise<DraftRecord | null> {
/**
* 결재 문서 삭제 (임시저장 상태만)
*/
export async function deleteDraft(id: string): Promise<{ success: boolean; error?: string }> {
export async function deleteDraft(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}`,
{
method: 'DELETE',
headers,
}
{ method: 'DELETE' }
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || '결재 문서 삭제에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -382,19 +350,24 @@ export async function deleteDrafts(ids: string[]): Promise<{ success: boolean; f
/**
* 결재 상신
*/
export async function submitDraft(id: string): Promise<{ success: boolean; error?: string }> {
export async function submitDraft(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/submit`,
{
method: 'POST',
headers,
body: JSON.stringify({}),
}
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || '결재 상신에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -441,19 +414,24 @@ export async function submitDrafts(ids: string[]): Promise<{ success: boolean; f
/**
* 결재 회수 (기안자만)
*/
export async function cancelDraft(id: string): Promise<{ success: boolean; error?: string }> {
export async function cancelDraft(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/cancel`,
{
method: 'POST',
headers,
body: JSON.stringify({}),
}
);
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || '결재 회수에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {

View File

@@ -9,8 +9,8 @@
'use server';
import { cookies } from 'next/headers';
import type { ReferenceRecord, ApprovalType, DocumentStatus, ReadStatus } from './types';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { ReferenceRecord, ApprovalType, DocumentStatus } from './types';
// ============================================
// API 응답 타입 정의
@@ -72,21 +72,6 @@ interface ReferenceStepApiData {
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* API 상태 → 프론트엔드 상태 변환
*/
@@ -156,7 +141,6 @@ export async function getReferences(params?: {
sort_dir?: 'asc' | 'desc';
}): Promise<{ data: ReferenceRecord[]; total: number; lastPage: number }> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -173,12 +157,16 @@ export async function getReferences(params?: {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/reference?${searchParams.toString()}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
// serverFetch handles 401 with redirect, so we just check for other errors
if (error || !response) {
console.error('[ReferenceBoxActions] GET reference error:', error?.message);
return { data: [], total: 0, lastPage: 1 };
}
if (!response.ok) {
console.error('[ReferenceBoxActions] GET reference error:', response.status);
return { data: [], total: 0, lastPage: 1 };
@@ -228,16 +216,20 @@ export async function getReferenceSummary(): Promise<{ all: number; read: number
*/
export async function markAsRead(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/read`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/read`,
{
method: 'POST',
headers,
body: JSON.stringify({}),
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify({}),
});
// serverFetch handles 401 with redirect
if (error || !response) {
return {
success: false,
error: error?.message || '열람 처리에 실패했습니다.',
};
}
const result = await response.json();
@@ -263,16 +255,20 @@ export async function markAsRead(id: string): Promise<{ success: boolean; error?
*/
export async function markAsUnread(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/unread`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${id}/unread`,
{
method: 'POST',
headers,
body: JSON.stringify({}),
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify({}),
});
// serverFetch handles 401 with redirect
if (error || !response) {
return {
success: false,
error: error?.message || '미열람 처리에 실패했습니다.',
};
}
const result = await response.json();

View File

@@ -5,7 +5,6 @@ import {
Files,
Eye,
EyeOff,
Check,
BookOpen,
} from 'lucide-react';
import { toast } from 'sonner';
@@ -44,19 +43,22 @@ import {
} from '@/components/templates/IntegratedListTemplateV2';
import { DateRangeSelector } from '@/components/molecules/DateRangeSelector';
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
import { DocumentDetailModal } from '@/components/approval/DocumentDetail';
import type { DocumentType, ProposalDocumentData, ExpenseReportDocumentData, ExpenseEstimateDocumentData } from '@/components/approval/DocumentDetail/types';
import type {
ReferenceTabType,
ReferenceRecord,
SortOption,
FilterOption,
ApprovalType,
} from './types';
import {
REFERENCE_TAB_LABELS,
SORT_OPTIONS,
FILTER_OPTIONS,
APPROVAL_TYPE_LABELS,
DOCUMENT_STATUS_LABELS,
DOCUMENT_STATUS_COLORS,
READ_STATUS_LABELS,
READ_STATUS_COLORS,
} from './types';
// ===== 통계 타입 =====
@@ -86,6 +88,10 @@ export function ReferenceBox() {
const [markReadDialogOpen, setMarkReadDialogOpen] = useState(false);
const [markUnreadDialogOpen, setMarkUnreadDialogOpen] = useState(false);
// ===== 문서 상세 모달 상태 =====
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedDocument, setSelectedDocument] = useState<ReferenceRecord | null>(null);
// API 데이터
const [data, setData] = useState<ReferenceRecord[]>([]);
const [totalCount, setTotalCount] = useState(0);
@@ -252,6 +258,87 @@ export function ReferenceBox() {
setMarkUnreadDialogOpen(false);
}, [selectedItems, loadData, loadSummary]);
// ===== 문서 클릭/상세 보기 핸들러 =====
const handleDocumentClick = useCallback((item: ReferenceRecord) => {
setSelectedDocument(item);
setIsModalOpen(true);
}, []);
// ===== ApprovalType → DocumentType 변환 =====
const getDocumentType = (approvalType: ApprovalType): DocumentType => {
switch (approvalType) {
case 'expense_estimate': return 'expenseEstimate';
case 'expense_report': return 'expenseReport';
default: return 'proposal';
}
};
// ===== ReferenceRecord → 모달용 데이터 변환 =====
const convertToModalData = (item: ReferenceRecord): ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData => {
const docType = getDocumentType(item.approvalType);
const drafter = {
id: 'drafter-1',
name: item.drafter,
position: item.drafterPosition,
department: item.drafterDepartment,
status: 'approved' as const,
};
const approvers = [{
id: 'approver-1',
name: '결재자',
position: '부장',
department: '경영지원팀',
status: 'approved' as const,
}];
switch (docType) {
case 'expenseEstimate':
return {
documentNo: item.documentNo,
createdAt: item.draftDate,
items: [
{ id: '1', expectedPaymentDate: '2025-11-05', category: '통신 서비스', amount: 550000, vendor: 'KT', account: '국민 123-456-789012 홍길동' },
{ id: '2', expectedPaymentDate: '2025-11-12', category: '인건 대행', amount: 2500000, vendor: '에이치알코리아', account: '신한 110-123-456789 (주)에이치알' },
],
totalExpense: 3050000,
accountBalance: 25000000,
finalDifference: 21950000,
approvers,
drafter,
};
case 'expenseReport':
return {
documentNo: item.documentNo,
createdAt: item.draftDate,
requestDate: item.draftDate,
paymentDate: item.draftDate,
items: [
{ id: '1', no: 1, description: '업무용 택시비', amount: 50000, note: '고객사 미팅' },
{ id: '2', no: 2, description: '식대', amount: 30000, note: '팀 회식' },
],
cardInfo: '삼성카드 **** 1234',
totalAmount: 80000,
attachments: [],
approvers,
drafter,
};
default:
return {
documentNo: item.documentNo,
createdAt: item.draftDate,
vendor: '거래처',
vendorPaymentDate: item.draftDate,
title: item.title,
description: item.title,
reason: '업무상 필요',
estimatedCost: 1000000,
attachments: [],
approvers,
drafter,
};
}
};
// ===== 통계 카드 =====
const statCards: StatCard[] = useMemo(() => [
{ label: '전체', value: `${stats.all}`, icon: Files, iconColor: 'text-blue-500' },
@@ -267,7 +354,7 @@ export function ReferenceBox() {
], [stats]);
// ===== 테이블 컬럼 =====
// 문서번호, 문서유형, 제목, 기안자, 기안일시, 상태, 확인
// 문서번호, 문서유형, 제목, 기안자, 기안일시, 상태
const tableColumns: TableColumn[] = useMemo(() => [
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
{ key: 'documentNo', label: '문서번호' },
@@ -276,7 +363,6 @@ export function ReferenceBox() {
{ key: 'drafter', label: '기안자' },
{ key: 'draftDate', label: '기안일시' },
{ key: 'status', label: '상태', className: 'text-center' },
{ key: 'confirm', label: '확인', className: 'w-[80px] text-center' },
], []);
// ===== 테이블 행 렌더링 =====
@@ -284,8 +370,12 @@ export function ReferenceBox() {
const isSelected = selectedItems.has(item.id);
return (
<TableRow key={item.id} className="hover:bg-muted/50">
<TableCell className="text-center">
<TableRow
key={item.id}
className="hover:bg-muted/50 cursor-pointer"
onClick={() => handleDocumentClick(item)}
>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox checked={isSelected} onCheckedChange={() => toggleSelection(item.id)} />
</TableCell>
<TableCell className="text-center">{globalIndex}</TableCell>
@@ -297,18 +387,13 @@ export function ReferenceBox() {
<TableCell>{item.drafter}</TableCell>
<TableCell>{item.draftDate}</TableCell>
<TableCell className="text-center">
<Badge className={DOCUMENT_STATUS_COLORS[item.documentStatus]}>
{DOCUMENT_STATUS_LABELS[item.documentStatus]}
<Badge className={READ_STATUS_COLORS[item.readStatus]}>
{READ_STATUS_LABELS[item.readStatus]}
</Badge>
</TableCell>
<TableCell className="text-center">
{item.readStatus === 'read' && (
<Check className="h-4 w-4 mx-auto text-green-600" />
)}
</TableCell>
</TableRow>
);
}, [selectedItems, toggleSelection]);
}, [selectedItems, toggleSelection, handleDocumentClick]);
// ===== 모바일 카드 렌더링 =====
const renderMobileCard = useCallback((
@@ -325,14 +410,9 @@ export function ReferenceBox() {
headerBadges={
<div className="flex gap-1">
<Badge variant="outline">{APPROVAL_TYPE_LABELS[item.approvalType]}</Badge>
<Badge className={DOCUMENT_STATUS_COLORS[item.documentStatus]}>
{DOCUMENT_STATUS_LABELS[item.documentStatus]}
<Badge className={READ_STATUS_COLORS[item.readStatus]}>
{READ_STATUS_LABELS[item.readStatus]}
</Badge>
{item.readStatus === 'read' ? (
<Badge className="bg-gray-100 text-gray-800"></Badge>
) : (
<Badge className="bg-blue-100 text-blue-800"></Badge>
)}
</div>
}
isSelected={isSelected}
@@ -505,6 +585,17 @@ export function ReferenceBox() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 문서 상세 모달 */}
{selectedDocument && (
<DocumentDetailModal
open={isModalOpen}
onOpenChange={setIsModalOpen}
documentType={getDocumentType(selectedDocument.approvalType)}
data={convertToModalData(selectedDocument)}
mode="reference"
/>
)}
</>
);
}

View File

@@ -9,7 +9,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
// ============================================
// 타입 정의
@@ -75,21 +75,6 @@ interface PaginatedResponse<T> {
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* API 응답에서 프론트엔드 형식으로 변환
*/
@@ -117,12 +102,10 @@ function transformApiToFrontend(apiData: Record<string, unknown>): AttendanceRec
*/
export async function checkIn(
data: CheckInRequest
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/attendances/check-in`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/attendances/check-in`, {
method: 'POST',
headers,
body: JSON.stringify({
user_id: data.userId,
check_in: data.checkIn,
@@ -136,6 +119,14 @@ export async function checkIn(
}),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '출근 기록에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
if (result.success && result.data) {
@@ -164,12 +155,10 @@ export async function checkIn(
*/
export async function checkOut(
data: CheckOutRequest
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/attendances/check-out`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/attendances/check-out`, {
method: 'POST',
headers,
body: JSON.stringify({
user_id: data.userId,
check_out: data.checkOut,
@@ -183,6 +172,14 @@ export async function checkOut(
}),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '퇴근 기록에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
if (result.success && result.data) {
@@ -213,19 +210,26 @@ export async function getTodayAttendance(): Promise<{
success: boolean;
data?: AttendanceRecord;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const today = new Date().toISOString().split('T')[0];
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.API_URL}/v1/attendances?date=${today}&per_page=1`,
{
method: 'GET',
headers,
}
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '근태 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
if (result.success && result.data) {

View File

@@ -12,7 +12,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { Board, BoardApiData, BoardFormData } from './types';
// API 응답 타입
@@ -22,21 +22,6 @@ interface ApiResponse<T> {
message: string;
}
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* API 데이터 → 프론트엔드 타입 변환
*/
@@ -89,10 +74,8 @@ function transformFrontendToApi(data: BoardFormData & { boardCode?: string; desc
export async function getBoards(filters?: {
board_type?: string;
search?: string;
}): Promise<{ success: boolean; data?: Board[]; error?: string }> {
}): Promise<{ success: boolean; data?: Board[]; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const params = new URLSearchParams();
if (filters?.board_type) params.append('board_type', filters.board_type);
if (filters?.search) params.append('search', filters.search);
@@ -101,14 +84,16 @@ export async function getBoards(filters?: {
// 테넌트 게시판만 조회 (시스템 게시판은 mng에서 관리)
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.error('[BoardActions] GET boards error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시판 목록 조회에 실패했습니다.' };
}
@@ -120,7 +105,7 @@ export async function getBoards(filters?: {
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!result.success) {
if (!response.ok || !result.success) {
return { success: false, error: result.message || '게시판 목록 조회에 실패했습니다.' };
}
@@ -143,10 +128,8 @@ export async function getBoards(filters?: {
export async function getTenantBoards(filters?: {
board_type?: string;
search?: string;
}): Promise<{ success: boolean; data?: Board[]; error?: string }> {
}): Promise<{ success: boolean; data?: Board[]; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const params = new URLSearchParams();
if (filters?.board_type) params.append('board_type', filters.board_type);
if (filters?.search) params.append('search', filters.search);
@@ -154,20 +137,28 @@ export async function getTenantBoards(filters?: {
const queryString = params.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/tenant${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.error('[BoardActions] GET tenant boards error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '테넌트 게시판 목록 조회에 실패했습니다.' };
}
const result: ApiResponse<BoardApiData[]> = await response.json();
let result: ApiResponse<BoardApiData[]>;
try {
result = await response.json();
} catch {
console.error('[BoardActions] JSON parse error');
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!result.success) {
if (!response.ok || !result.success) {
return { success: false, error: result.message || '테넌트 게시판 목록 조회에 실패했습니다.' };
}
@@ -182,27 +173,32 @@ export async function getTenantBoards(filters?: {
/**
* 게시판 상세 조회 (코드 기반)
*/
export async function getBoardByCode(code: string): Promise<{ success: boolean; data?: Board; error?: string }> {
export async function getBoardByCode(code: string): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${code}`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${code}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
const { response, error } = await serverFetch(url, {
method: 'GET',
cache: 'no-store',
});
if (!response.ok) {
console.error('[BoardActions] GET board error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시판 조회에 실패했습니다.' };
}
const result: ApiResponse<BoardApiData> = await response.json();
let result: ApiResponse<BoardApiData>;
try {
result = await response.json();
} catch {
console.error('[BoardActions] JSON parse error');
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!result.success || !result.data) {
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '게시판을 찾을 수 없습니다.' };
}
@@ -216,27 +212,32 @@ export async function getBoardByCode(code: string): Promise<{ success: boolean;
/**
* 게시판 상세 조회 (ID 기반)
*/
export async function getBoardById(id: string): Promise<{ success: boolean; data?: Board; error?: string }> {
export async function getBoardById(id: string): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
const { response, error } = await serverFetch(url, {
method: 'GET',
cache: 'no-store',
});
if (!response.ok) {
console.error('[BoardActions] GET board by id error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시판 조회에 실패했습니다.' };
}
const result: ApiResponse<BoardApiData> = await response.json();
let result: ApiResponse<BoardApiData>;
try {
result = await response.json();
} catch {
console.error('[BoardActions] JSON parse error');
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!result.success || !result.data) {
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '게시판을 찾을 수 없습니다.' };
}
@@ -252,21 +253,31 @@ export async function getBoardById(id: string): Promise<{ success: boolean; data
*/
export async function createBoard(
data: BoardFormData & { boardCode: string; description?: string }
): Promise<{ success: boolean; data?: Board; error?: string }> {
): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(apiData),
});
const result = await response.json();
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시판 생성에 실패했습니다.' };
}
let result: ApiResponse<BoardApiData>;
try {
result = await response.json();
} catch {
console.error('[BoardActions] JSON parse error');
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!response.ok || !result.success) {
return {
@@ -291,21 +302,31 @@ export async function createBoard(
export async function updateBoard(
id: string,
data: BoardFormData & { boardCode?: string; description?: string }
): Promise<{ success: boolean; data?: Board; error?: string }> {
): Promise<{ success: boolean; data?: Board; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data, true); // isUpdate=true
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(apiData),
});
const result = await response.json();
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시판 수정에 실패했습니다.' };
}
let result: ApiResponse<BoardApiData>;
try {
result = await response.json();
} catch {
console.error('[BoardActions] JSON parse error');
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!response.ok || !result.success) {
return {
@@ -327,19 +348,29 @@ export async function updateBoard(
/**
* 게시판 삭제
*/
export async function deleteBoard(id: string): Promise<{ success: boolean; error?: string }> {
export async function deleteBoard(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${id}`,
{
method: 'DELETE',
headers,
}
);
const { response, error } = await serverFetch(url, {
method: 'DELETE',
});
const result = await response.json();
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시판 삭제에 실패했습니다.' };
}
let result: { success: boolean; message?: string };
try {
result = await response.json();
} catch {
console.error('[BoardActions] JSON parse error');
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!response.ok || !result.success) {
return {
@@ -358,10 +389,15 @@ export async function deleteBoard(id: string): Promise<{ success: boolean; error
/**
* 게시판 일괄 삭제
*/
export async function deleteBoardsBulk(ids: string[]): Promise<{ success: boolean; error?: string }> {
export async function deleteBoardsBulk(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const results = await Promise.all(ids.map(id => deleteBoard(id)));
const failed = results.filter(r => !r.success);
const hasAuthError = results.some(r => r.__authError);
if (hasAuthError) {
return { success: false, error: '인증이 만료되었습니다.', __authError: true };
}
if (failed.length > 0) {
return {

View File

@@ -12,7 +12,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
PostApiData,
PostPaginationResponse,
@@ -21,21 +21,6 @@ import type {
Post,
} from './types';
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* API 데이터 → 프론트엔드 타입 변환
*/
@@ -74,10 +59,8 @@ function transformApiToPost(apiData: PostApiData, boardName?: string): Post {
export async function getPosts(
boardCode: string,
filters?: PostFilters
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string }> {
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const params = new URLSearchParams();
if (filters?.search) params.append('search', filters.search);
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
@@ -88,14 +71,16 @@ export async function getPosts(
const queryString = params.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.error('[BoardActions] GET posts error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 목록 조회에 실패했습니다.' };
}
@@ -107,7 +92,7 @@ export async function getPosts(
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!result.success) {
if (!response.ok || !result.success) {
return { success: false, error: result.message || '게시글 목록 조회에 실패했습니다.' };
}
@@ -126,10 +111,8 @@ export async function getPosts(
*/
export async function getMyPosts(
filters?: PostFilters
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string }> {
): Promise<{ success: boolean; data?: PostPaginationResponse; posts?: Post[]; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const params = new URLSearchParams();
if (filters?.search) params.append('search', filters.search);
if (filters?.board_code) params.append('board_code', filters.board_code);
@@ -140,14 +123,16 @@ export async function getMyPosts(
const queryString = params.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/my-posts${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.error('[BoardActions] GET my-posts error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '나의 게시글 조회에 실패했습니다.' };
}
@@ -159,7 +144,7 @@ export async function getMyPosts(
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!result.success) {
if (!response.ok || !result.success) {
return { success: false, error: result.message || '나의 게시글 조회에 실패했습니다.' };
}
@@ -179,26 +164,26 @@ export async function getMyPosts(
export async function getPost(
boardCode: string,
postId: number | string
): Promise<{ success: boolean; data?: Post; error?: string }> {
): Promise<{ success: boolean; data?: Post; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.error('[BoardActions] GET post error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 조회에 실패했습니다.' };
}
const result: ApiResponse<PostApiData> = await response.json();
if (!result.success || !result.data) {
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '게시글을 찾을 수 없습니다.' };
}
@@ -221,25 +206,27 @@ export async function createPost(
is_secret?: boolean;
custom_fields?: Record<string, string>;
}
): Promise<{ success: boolean; data?: Post; error?: string }> {
): Promise<{ success: boolean; data?: Post; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 등록에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '게시글 등록에 실패했습니다.',
};
return { success: false, error: result.message || '게시글 등록에 실패했습니다.' };
}
return { success: true, data: transformApiToPost(result.data) };
@@ -262,25 +249,27 @@ export async function updatePost(
is_secret?: boolean;
custom_fields?: Record<string, string>;
}
): Promise<{ success: boolean; data?: Post; error?: string }> {
): Promise<{ success: boolean; data?: Post; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'PUT',
headers,
body: JSON.stringify(data),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 수정에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '게시글 수정에 실패했습니다.',
};
return { success: false, error: result.message || '게시글 수정에 실패했습니다.' };
}
return { success: true, data: transformApiToPost(result.data) };
@@ -296,24 +285,26 @@ export async function updatePost(
export async function deletePost(
boardCode: string,
postId: number | string
): Promise<{ success: boolean; error?: string }> {
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/boards/${boardCode}/posts/${postId}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'DELETE',
headers,
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 삭제에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '게시글 삭제에 실패했습니다.',
};
return { success: false, error: result.message || '게시글 삭제에 실패했습니다.' };
}
return { success: true };
@@ -321,4 +312,4 @@ export async function deletePost(
console.error('[BoardActions] deletePost error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
}

View File

@@ -0,0 +1,19 @@
'use client';
import { Suspense } from "react";
import { JuilMainDashboard } from "./JuilMainDashboard";
import { PageLoadingSpinner } from "@/components/ui/loading-spinner";
/**
* JuilDashboard - 주일기업 전용 대시보드
*
* 건설/공사 프로젝트 중심의 메트릭과 현황을 보여줍니다.
*/
export function JuilDashboard() {
console.log('🏗️ Juil Dashboard rendering...');
return (
<Suspense fallback={<PageLoadingSpinner text="공사 현황을 불러오는 중..." />}>
<JuilMainDashboard />
</Suspense>
);
}

View File

@@ -0,0 +1,195 @@
'use client';
import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useCurrentTime } from "@/hooks/useCurrentTime";
import {
Building2,
MapPin,
HardHat,
Truck,
AlertTriangle,
FileText,
Calendar as CalendarIcon,
CheckCircle2,
Clock,
ArrowUpRight,
ClipboardList,
Hammer
} from "lucide-react";
export function JuilMainDashboard() {
const currentTime = useCurrentTime();
// 가상 데이터: 건설 프로젝트 현황
const projectStats = {
totalProjects: 12,
active: 5, // 진행중
bidding: 3, // 입찰중
planning: 4, // 설계/계획
};
const recentProjects = [
{ id: 1, name: "강남 데이터센터 신축공사", status: "공사중", progress: 45, manager: "김현장", deadline: "2024-12-30" },
{ id: 2, name: "판교 오피스 리모델링", status: "마감", progress: 95, manager: "이공사", deadline: "2024-06-15" },
{ id: 3, name: "부산 물류센터 증축", status: "착공준비", progress: 5, manager: "박안전", deadline: "2025-03-20" },
];
const upcomingEvents = [
{ id: 1, type: "현장설명회", title: "송도 주상복합 입찰 설명회", date: "2024-06-25 14:00", location: "인천 송도 현장" },
{ id: 2, type: "입찰마감", title: "세종 스마트시티 관로공사", date: "2024-06-28 17:00", location: "전자조달" },
];
return (
<div className="p-4 md:p-6 space-y-6">
{/* 헤더 섹션 */}
<div className="bg-card border border-border/20 rounded-xl p-4 md:p-6 mb-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<Building2 className="w-8 h-8 text-blue-600" />
</h1>
<p className="text-sm text-muted-foreground mt-1">
{currentTime} ·
</p>
</div>
<div className="flex gap-2">
<Button className="bg-blue-600 hover:bg-blue-700">
<ClipboardList className="w-4 h-4 mr-2" />
</Button>
</div>
</div>
</div>
{/* 상단 통계 카드 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<MapPin className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projectStats.active}</div>
<p className="text-xs text-muted-foreground mt-1">
{projectStats.totalProjects}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<HardHat className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">142</div>
<p className="text-xs text-muted-foreground mt-1">
<span className="text-green-500 font-medium">+12</span> ( )
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projectStats.bidding}</div>
<p className="text-xs text-muted-foreground mt-1">
1
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"> </CardTitle>
<AlertTriangle className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">0</div>
<p className="text-xs text-muted-foreground mt-1">
125
</p>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* 주요 프로젝트 진행 현황 */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Hammer className="w-5 h-5" />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{recentProjects.map((project) => (
<div key={project.id} className="flex items-center justify-between p-4 bg-muted/40 rounded-lg border">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-bold">{project.name}</span>
<Badge variant={project.status === '공사중' ? 'default' : 'secondary'}>
{project.status}
</Badge>
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<HardHat className="w-3 h-3" /> {project.manager}
</span>
<span className="flex items-center gap-1">
<CalendarIcon className="w-3 h-3" /> {project.deadline}
</span>
</div>
</div>
<div className="w-32 text-right">
<div className="text-2xl font-bold text-blue-600">{project.progress}%</div>
<div className="text-xs text-muted-foreground"></div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* 일정 / 알림 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="w-5 h-5" />
(/)
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{upcomingEvents.map((event) => (
<div key={event.id} className="flex gap-3 items-start border-l-4 border-blue-200 pl-3 py-1">
<div className="flex-1">
<Badge variant="outline" className="mb-1 text-xs">
{event.type}
</Badge>
<p className="font-medium text-sm">{event.title}</p>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
<span>{event.date}</span>
<span>|</span>
<span>{event.location}</span>
</div>
</div>
</div>
))}
<Button variant="ghost" className="w-full text-sm text-muted-foreground">
<ArrowUpRight className="w-4 h-4 ml-1" />
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,493 @@
'use client';
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Building2, Plus, Pencil, Trash2, AlertTriangle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { TableCell, TableRow } from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { IntegratedListTemplateV2, TabOption, TableColumn } from '@/components/templates/IntegratedListTemplateV2';
import { MobileCard } from '@/components/molecules/MobileCard';
import { toast } from 'sonner';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import type { Partner, PartnerStats } from './types';
import { getPartnerList, deletePartner, deletePartners, getPartnerStats } from './actions';
// 테이블 컬럼 정의
const tableColumns: TableColumn[] = [
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
{ key: 'partnerCode', label: '거래처번호', className: 'w-[100px]' },
{ key: 'category', label: '구분', className: 'w-[80px] text-center' },
{ key: 'partnerName', label: '거래처명', className: 'min-w-[150px]' },
{ key: 'representative', label: '대표자', className: 'w-[100px]' },
{ key: 'manager', label: '담당자', className: 'w-[100px]' },
{ key: 'phone', label: '전화번호', className: 'w-[130px]' },
{ key: 'paymentDay', label: '매출 결제일', className: 'w-[100px] text-center' },
{ key: 'isBadDebt', label: '악성채권', className: 'w-[90px] text-center' },
{ key: 'actions', label: '작업', className: 'w-[100px] text-center' },
];
interface PartnerListClientProps {
initialData?: Partner[];
initialStats?: PartnerStats;
}
export default function PartnerListClient({ initialData = [], initialStats }: PartnerListClientProps) {
const router = useRouter();
// 상태
const [partners, setPartners] = useState<Partner[]>(initialData);
const [stats, setStats] = useState<PartnerStats>(
initialStats ?? { total: 0, unregistered: 0, badDebt: 0, normal: 0 }
);
const [activeTab, setActiveTab] = useState('all');
const [searchValue, setSearchValue] = useState('');
const [badDebtFilter, setBadDebtFilter] = useState<'all' | 'badDebt' | 'normal'>('all');
const [sortBy, setSortBy] = useState<'latest' | 'oldest' | 'nameAsc' | 'nameDesc'>('latest');
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const [currentPage, setCurrentPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null);
const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false);
const itemsPerPage = 20;
// 데이터 로드
const loadData = useCallback(async () => {
setIsLoading(true);
try {
const [listResult, statsResult] = await Promise.all([
getPartnerList({ size: 1000, badDebtFilter, sortBy }),
getPartnerStats(),
]);
if (listResult.success && listResult.data) {
setPartners(listResult.data.items);
}
if (statsResult.success && statsResult.data) {
setStats(statsResult.data);
}
} catch {
toast.error('데이터 로드에 실패했습니다.');
} finally {
setIsLoading(false);
}
}, [badDebtFilter, sortBy]);
// 초기 데이터가 없으면 로드
useEffect(() => {
if (initialData.length === 0) {
loadData();
}
}, [initialData.length, loadData]);
// 필터링된 데이터
const filteredPartners = useMemo(() => {
return partners.filter((partner) => {
// 탭 필터 (전체/신규)
if (activeTab === 'new') {
// 신규 조건 (예: 최근 7일 이내 등록)
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
if (new Date(partner.createdAt) < sevenDaysAgo) {
return false;
}
}
// 악성채권 필터
if (badDebtFilter === 'badDebt' && !partner.isBadDebt) {
return false;
}
if (badDebtFilter === 'normal' && partner.isBadDebt) {
return false;
}
// 검색 필터
if (searchValue) {
const search = searchValue.toLowerCase();
return (
partner.partnerCode.toLowerCase().includes(search) ||
partner.partnerName.toLowerCase().includes(search) ||
partner.representative.toLowerCase().includes(search) ||
partner.manager.toLowerCase().includes(search)
);
}
return true;
});
}, [partners, activeTab, badDebtFilter, searchValue]);
// 정렬
const sortedPartners = useMemo(() => {
const sorted = [...filteredPartners];
switch (sortBy) {
case 'latest':
sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
break;
case 'oldest':
sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
break;
case 'nameAsc':
sorted.sort((a, b) => a.partnerName.localeCompare(b.partnerName));
break;
case 'nameDesc':
sorted.sort((a, b) => b.partnerName.localeCompare(a.partnerName));
break;
}
return sorted;
}, [filteredPartners, sortBy]);
// 페이지네이션
const totalPages = Math.ceil(sortedPartners.length / itemsPerPage);
const paginatedData = useMemo(() => {
const start = (currentPage - 1) * itemsPerPage;
return sortedPartners.slice(start, start + itemsPerPage);
}, [sortedPartners, currentPage, itemsPerPage]);
// 탭 옵션
const tabOptions: TabOption[] = useMemo(
() => [
{ value: 'all', label: '전체', count: stats.total },
{ value: 'new', label: '신규', count: stats.unregistered },
],
[stats]
);
// 핸들러
const handleTabChange = useCallback((value: string) => {
setActiveTab(value);
setCurrentPage(1);
}, []);
const handleSearchChange = useCallback((value: string) => {
setSearchValue(value);
setCurrentPage(1);
}, []);
const handleToggleSelection = useCallback((id: string) => {
setSelectedItems((prev) => {
const newSet = new Set(prev);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}
return newSet;
});
}, []);
const handleToggleSelectAll = useCallback(() => {
if (selectedItems.size === paginatedData.length) {
setSelectedItems(new Set());
} else {
setSelectedItems(new Set(paginatedData.map((p) => p.id)));
}
}, [selectedItems.size, paginatedData]);
const handleRowClick = useCallback(
(partner: Partner) => {
router.push(`/ko/juil/project/bidding/partners/${partner.id}`);
},
[router]
);
const handleCreate = useCallback(() => {
router.push('/ko/juil/project/bidding/partners/new');
}, [router]);
const handleEdit = useCallback(
(e: React.MouseEvent, partnerId: string) => {
e.stopPropagation();
router.push(`/ko/juil/project/bidding/partners/${partnerId}/edit`);
},
[router]
);
const handleDeleteClick = useCallback((e: React.MouseEvent, partnerId: string) => {
e.stopPropagation();
setDeleteTargetId(partnerId);
setDeleteDialogOpen(true);
}, []);
const handleDeleteConfirm = useCallback(async () => {
if (!deleteTargetId) return;
setIsLoading(true);
try {
const result = await deletePartner(deleteTargetId);
if (result.success) {
toast.success('거래처가 삭제되었습니다.');
setPartners((prev) => prev.filter((p) => p.id !== deleteTargetId));
setSelectedItems((prev) => {
const newSet = new Set(prev);
newSet.delete(deleteTargetId);
return newSet;
});
// 통계 재조회
const statsResult = await getPartnerStats();
if (statsResult.success && statsResult.data) {
setStats(statsResult.data);
}
} else {
toast.error(result.error || '삭제에 실패했습니다.');
}
} catch {
toast.error('삭제 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
setDeleteDialogOpen(false);
setDeleteTargetId(null);
}
}, [deleteTargetId]);
const handleBulkDeleteClick = useCallback(() => {
if (selectedItems.size === 0) {
toast.warning('삭제할 항목을 선택해주세요.');
return;
}
setBulkDeleteDialogOpen(true);
}, [selectedItems.size]);
const handleBulkDeleteConfirm = useCallback(async () => {
if (selectedItems.size === 0) return;
setIsLoading(true);
try {
const ids = Array.from(selectedItems);
const result = await deletePartners(ids);
if (result.success) {
toast.success(`${result.deletedCount}개 항목이 삭제되었습니다.`);
await loadData();
setSelectedItems(new Set());
} else {
toast.error(result.error || '일괄 삭제에 실패했습니다.');
}
} catch {
toast.error('일괄 삭제 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
setBulkDeleteDialogOpen(false);
}
}, [selectedItems, loadData]);
// 테이블 행 렌더링
const renderTableRow = useCallback(
(partner: Partner, index: number, globalIndex: number) => {
const isSelected = selectedItems.has(partner.id);
return (
<TableRow
key={partner.id}
className="cursor-pointer hover:bg-muted/50"
onClick={() => handleRowClick(partner)}
>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={isSelected}
onCheckedChange={() => handleToggleSelection(partner.id)}
/>
</TableCell>
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
<TableCell className="font-medium">{partner.partnerCode}</TableCell>
<TableCell className="text-center">
<Badge variant="secondary">{partner.category}</Badge>
</TableCell>
<TableCell className="font-medium">{partner.partnerName}</TableCell>
<TableCell>{partner.representative}</TableCell>
<TableCell>{partner.manager}</TableCell>
<TableCell>{partner.phone}</TableCell>
<TableCell className="text-center">
{partner.paymentDay ? `${partner.paymentDay}` : '-'}
</TableCell>
<TableCell className="text-center">
{partner.isBadDebt ? (
<Badge variant="destructive"></Badge>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell className="text-center">
{isSelected && (
<div className="flex items-center justify-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => handleEdit(e, partner.id)}
>
<Pencil className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={(e) => handleDeleteClick(e, partner.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</TableCell>
</TableRow>
);
},
[selectedItems, handleToggleSelection, handleRowClick, handleEdit, handleDeleteClick]
);
// 모바일 카드 렌더링
const renderMobileCard = useCallback(
(partner: Partner, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void) => {
return (
<MobileCard
title={partner.partnerName}
subtitle={partner.partnerCode}
badge={partner.isBadDebt ? '악성채권' : undefined}
badgeVariant={partner.isBadDebt ? 'destructive' : 'secondary'}
isSelected={isSelected}
onToggle={onToggle}
onClick={() => handleRowClick(partner)}
details={[
{ label: '구분', value: partner.category },
{ label: '대표자', value: partner.representative },
{ label: '담당자', value: partner.manager },
{ label: '전화번호', value: partner.phone },
{ label: '매출 결제일', value: partner.paymentDay ? `${partner.paymentDay}` : '-' },
]}
/>
);
},
[handleRowClick]
);
// 헤더 액션 (필터 + 등록 버튼)
const headerActions = (
<div className="flex items-center gap-2">
{/* 악성채권 필터 */}
<Select value={badDebtFilter} onValueChange={(v) => setBadDebtFilter(v as typeof badDebtFilter)}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="badDebt"></SelectItem>
<SelectItem value="normal"></SelectItem>
</SelectContent>
</Select>
{/* 정렬 */}
<Select value={sortBy} onValueChange={(v) => setSortBy(v as typeof sortBy)}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="최신순" />
</SelectTrigger>
<SelectContent>
<SelectItem value="latest"></SelectItem>
<SelectItem value="oldest"></SelectItem>
<SelectItem value="nameAsc"> </SelectItem>
<SelectItem value="nameDesc"> </SelectItem>
</SelectContent>
</Select>
{/* 등록 버튼 */}
<Button onClick={handleCreate}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
);
return (
<>
<IntegratedListTemplateV2
title="거래처 관리"
description="거래처 정보를 관리합니다"
icon={Building2}
headerActions={headerActions}
stats={[
{
label: '전체 거래처',
value: stats.total,
icon: Building2,
iconColor: 'text-blue-500',
},
{
label: '미등록',
value: stats.unregistered,
icon: AlertTriangle,
iconColor: 'text-orange-500',
},
]}
tabs={tabOptions}
activeTab={activeTab}
onTabChange={handleTabChange}
searchValue={searchValue}
onSearchChange={handleSearchChange}
searchPlaceholder="거래처명, 거래처번호, 대표자 검색"
tableColumns={tableColumns}
data={paginatedData}
allData={sortedPartners}
getItemId={(item) => item.id}
renderTableRow={renderTableRow}
renderMobileCard={renderMobileCard}
selectedItems={selectedItems}
onToggleSelection={handleToggleSelection}
onToggleSelectAll={handleToggleSelectAll}
onBulkDelete={handleBulkDeleteClick}
pagination={{
currentPage,
totalPages,
totalItems: sortedPartners.length,
itemsPerPage,
onPageChange: setCurrentPage,
}}
/>
{/* 단일 삭제 다이얼로그 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
? .
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteConfirm}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 일괄 삭제 다이얼로그 */}
<AlertDialog open={bulkDeleteDialogOpen} onOpenChange={setBulkDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription>
{selectedItems.size} ? .
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={handleBulkDeleteConfirm}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

View File

@@ -0,0 +1,223 @@
'use server';
import type { Partner, PartnerStats, PartnerFilter, PartnerListResponse } from './types';
/**
* 주일 기업 - 거래처 관리 Server Actions
* TODO: 실제 API 연동 시 구현
*/
// 목업 데이터
const mockPartners: Partner[] = [
{
id: '1',
partnerCode: '123123',
category: '매출',
partnerName: '회사명',
representative: '이름',
manager: '이름',
phone: '010-1234-1234',
paymentDay: 15,
isBadDebt: false,
isActive: true,
createdAt: '2025-01-01',
updatedAt: '2025-01-01',
},
{
id: '2',
partnerCode: '123123',
category: '매출',
partnerName: '회사명',
representative: '이름',
manager: '이름',
phone: '02-1234-1234',
paymentDay: 15,
isBadDebt: false,
isActive: true,
createdAt: '2025-01-02',
updatedAt: '2025-01-02',
},
{
id: '3',
partnerCode: '123123',
category: '매출',
partnerName: '회사명',
representative: '이름',
manager: '이름',
phone: '010-1234-1234',
paymentDay: 15,
isBadDebt: true,
isActive: true,
createdAt: '2025-01-03',
updatedAt: '2025-01-03',
},
{
id: '4',
partnerCode: '123123',
category: '매출',
partnerName: '회사명',
representative: '이름',
manager: '이름',
phone: '02-1234-1234',
paymentDay: 15,
isBadDebt: false,
isActive: true,
createdAt: '2025-01-04',
updatedAt: '2025-01-04',
},
{
id: '5',
partnerCode: '123123',
category: '매출',
partnerName: '회사명',
representative: '이름',
manager: '이름',
phone: '010-1234-1234',
paymentDay: 15,
isBadDebt: false,
isActive: true,
createdAt: '2025-01-05',
updatedAt: '2025-01-05',
},
{
id: '6',
partnerCode: '123123',
category: '매출',
partnerName: '회사명',
representative: '이름',
manager: '이름',
phone: '010-1234-1234',
paymentDay: 15,
isBadDebt: false,
isActive: true,
createdAt: '2025-01-06',
updatedAt: '2025-01-06',
},
{
id: '7',
partnerCode: '123123',
category: '매출',
partnerName: '회사명',
representative: '이름',
manager: '이름',
phone: '010-1234-1234',
paymentDay: 15,
isBadDebt: false,
isActive: true,
createdAt: '2025-01-07',
updatedAt: '2025-01-07',
},
];
// 거래처 목록 조회
export async function getPartnerList(
filter?: PartnerFilter
): Promise<{ success: boolean; data?: PartnerListResponse; error?: string }> {
try {
// TODO: 실제 API 호출
// const response = await fetch(`${API_URL}/partners`, { ... });
let filtered = [...mockPartners];
// 검색 필터
if (filter?.search) {
const search = filter.search.toLowerCase();
filtered = filtered.filter(
(p) =>
p.partnerName.toLowerCase().includes(search) ||
p.partnerCode.toLowerCase().includes(search) ||
p.representative.toLowerCase().includes(search)
);
}
// 악성채권 필터
if (filter?.badDebtFilter && filter.badDebtFilter !== 'all') {
filtered = filtered.filter((p) =>
filter.badDebtFilter === 'badDebt' ? p.isBadDebt : !p.isBadDebt
);
}
// 정렬
if (filter?.sortBy) {
switch (filter.sortBy) {
case 'latest':
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
break;
case 'oldest':
filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
break;
case 'nameAsc':
filtered.sort((a, b) => a.partnerName.localeCompare(b.partnerName));
break;
case 'nameDesc':
filtered.sort((a, b) => b.partnerName.localeCompare(a.partnerName));
break;
}
}
const page = filter?.page ?? 1;
const size = filter?.size ?? 20;
const start = (page - 1) * size;
const paginatedItems = filtered.slice(start, start + size);
return {
success: true,
data: {
items: paginatedItems,
total: filtered.length,
page,
size,
totalPages: Math.ceil(filtered.length / size),
},
};
} catch (error) {
console.error('getPartnerList error:', error);
return { success: false, error: '거래처 목록 조회에 실패했습니다.' };
}
}
// 거래처 통계 조회
export async function getPartnerStats(): Promise<{ success: boolean; data?: PartnerStats; error?: string }> {
try {
// TODO: 실제 API 호출
const total = mockPartners.length;
const badDebt = mockPartners.filter((p) => p.isBadDebt).length;
return {
success: true,
data: {
total,
unregistered: 5, // 목업
badDebt,
normal: total - badDebt,
},
};
} catch (error) {
console.error('getPartnerStats error:', error);
return { success: false, error: '통계 조회에 실패했습니다.' };
}
}
// 거래처 삭제
export async function deletePartner(id: string): Promise<{ success: boolean; error?: string }> {
try {
// TODO: 실제 API 호출
console.log('Delete partner:', id);
return { success: true };
} catch (error) {
console.error('deletePartner error:', error);
return { success: false, error: '거래처 삭제에 실패했습니다.' };
}
}
// 거래처 일괄 삭제
export async function deletePartners(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
try {
// TODO: 실제 API 호출
console.log('Delete partners:', ids);
return { success: true, deletedCount: ids.length };
} catch (error) {
console.error('deletePartners error:', error);
return { success: false, error: '일괄 삭제에 실패했습니다.' };
}
}

View File

@@ -0,0 +1,3 @@
export { default as PartnerListClient } from './PartnerListClient';
export * from './types';
export * from './actions';

View File

@@ -0,0 +1,57 @@
/**
* 주일 기업 - 거래처 관리 타입 정의
*/
// 거래처 타입
export interface Partner {
id: string;
partnerCode: string; // 거래처번호
category: string; // 구분 (건설사, 시공사 등)
partnerName: string; // 거래처명
representative: string; // 대표자
manager: string; // 담당자
phone: string; // 전화번호
paymentDay: number | null; // 매출 결제일
isBadDebt: boolean; // 악성채권 여부
isActive: boolean; // 활성 상태
createdAt: string;
updatedAt: string;
}
// 거래처 통계
export interface PartnerStats {
total: number; // 전체 거래처
unregistered: number; // 미등록
badDebt: number; // 악성채권
normal: number; // 정상
}
// 거래처 필터
export interface PartnerFilter {
search?: string;
badDebtFilter?: 'all' | 'badDebt' | 'normal';
sortBy?: 'latest' | 'oldest' | 'nameAsc' | 'nameDesc';
page?: number;
size?: number;
}
// 거래처 폼 데이터
export interface PartnerFormData {
partnerCode?: string;
category: string;
partnerName: string;
representative: string;
manager: string;
phone: string;
paymentDay: number | null;
isBadDebt: boolean;
}
// API 응답 타입
export interface PartnerListResponse {
items: Partner[];
total: number;
page: number;
size: number;
totalPages: number;
}

View File

@@ -5,7 +5,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
PostApiData,
PostPaginationResponse,
@@ -16,31 +16,14 @@ import type {
CommentsApiResponse,
} from './types';
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* 게시글 목록 조회
*/
export async function getPosts(
boardCode: SystemBoardCode,
filters?: PostFilters
): Promise<{ success: boolean; data?: PostPaginationResponse; error?: string }> {
): Promise<{ success: boolean; data?: PostPaginationResponse; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const params = new URLSearchParams();
if (filters?.search) params.append('search', filters.search);
if (filters?.is_notice !== undefined) params.append('is_notice', String(filters.is_notice));
@@ -51,14 +34,16 @@ export async function getPosts(
const queryString = params.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.error('[CustomerCenterActions] GET posts error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 목록 조회에 실패했습니다.' };
}
@@ -70,7 +55,7 @@ export async function getPosts(
return { success: false, error: '서버 응답 형식 오류입니다.' };
}
if (!result.success) {
if (!response.ok || !result.success) {
return { success: false, error: result.message || '게시글 목록 조회에 실패했습니다.' };
}
@@ -87,26 +72,26 @@ export async function getPosts(
export async function getPost(
boardCode: SystemBoardCode,
postId: number | string
): Promise<{ success: boolean; data?: PostApiData; error?: string }> {
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.error('[CustomerCenterActions] GET post error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 조회에 실패했습니다.' };
}
const result: ApiResponse<PostApiData> = await response.json();
if (!result.success || !result.data) {
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '게시글을 찾을 수 없습니다.' };
}
@@ -128,25 +113,27 @@ export async function createPost(
is_secret?: boolean;
custom_fields?: Record<string, string>;
}
): Promise<{ success: boolean; data?: PostApiData; error?: string }> {
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'POST',
headers,
body: JSON.stringify(data),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 등록에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '게시글 등록에 실패했습니다.',
};
return { success: false, error: result.message || '게시글 등록에 실패했습니다.' };
}
return { success: true, data: result.data };
@@ -168,25 +155,27 @@ export async function updatePost(
is_secret?: boolean;
custom_fields?: Record<string, string>;
}
): Promise<{ success: boolean; data?: PostApiData; error?: string }> {
): Promise<{ success: boolean; data?: PostApiData; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'PUT',
headers,
body: JSON.stringify(data),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 수정에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '게시글 수정에 실패했습니다.',
};
return { success: false, error: result.message || '게시글 수정에 실패했습니다.' };
}
return { success: true, data: result.data };
@@ -202,24 +191,26 @@ export async function updatePost(
export async function deletePost(
boardCode: SystemBoardCode,
postId: number | string
): Promise<{ success: boolean; error?: string }> {
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'DELETE',
headers,
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '게시글 삭제에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '게시글 삭제에 실패했습니다.',
};
return { success: false, error: result.message || '게시글 삭제에 실패했습니다.' };
}
return { success: true };
@@ -237,26 +228,26 @@ export async function deletePost(
export async function getComments(
boardCode: SystemBoardCode,
postId: number | string
): Promise<{ success: boolean; data?: CommentsApiResponse; error?: string }> {
): Promise<{ success: boolean; data?: CommentsApiResponse; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.error('[CustomerCenterActions] GET comments error:', response.status);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '댓글 목록 조회에 실패했습니다.' };
}
const result: ApiResponse<CommentsApiResponse> = await response.json();
if (!result.success) {
if (!response.ok || !result.success) {
return { success: false, error: result.message || '댓글 목록 조회에 실패했습니다.' };
}
@@ -274,25 +265,27 @@ export async function createComment(
boardCode: SystemBoardCode,
postId: number | string,
content: string
): Promise<{ success: boolean; data?: CommentApiData; error?: string }> {
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ content }),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '댓글 등록에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '댓글 등록에 실패했습니다.',
};
return { success: false, error: result.message || '댓글 등록에 실패했습니다.' };
}
return { success: true, data: result.data };
@@ -310,25 +303,27 @@ export async function updateComment(
postId: number | string,
commentId: number | string,
content: string
): Promise<{ success: boolean; data?: CommentApiData; error?: string }> {
): Promise<{ success: boolean; data?: CommentApiData; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments/${commentId}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'PUT',
headers,
body: JSON.stringify({ content }),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '댓글 수정에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '댓글 수정에 실패했습니다.',
};
return { success: false, error: result.message || '댓글 수정에 실패했습니다.' };
}
return { success: true, data: result.data };
@@ -345,24 +340,26 @@ export async function deleteComment(
boardCode: SystemBoardCode,
postId: number | string,
commentId: number | string
): Promise<{ success: boolean; error?: string }> {
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/system-boards/${boardCode}/posts/${postId}/comments/${commentId}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'DELETE',
headers,
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '댓글 삭제에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '댓글 삭제에 실패했습니다.',
};
return { success: false, error: result.message || '댓글 삭제에 실패했습니다.' };
}
return { success: true };
@@ -370,4 +367,4 @@ export async function deleteComment(
console.error('[CustomerCenterActions] deleteComment error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
}

View File

@@ -13,7 +13,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
AttendanceRecord,
AttendanceApiData,
@@ -44,20 +44,8 @@ interface PaginatedResponse<T> {
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// API URL
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
/**
* API 응답 데이터를 프론트엔드 형식으로 변환
@@ -172,45 +160,35 @@ interface EmployeeApiData {
* 사원 목록 조회 (근태 등록용)
*/
export async function getEmployeesForAttendance(): Promise<EmployeeOption[]> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
searchParams.set('per_page', '100'); // API 최대 500, 드롭다운용 100 충분
searchParams.set('status', 'active'); // 재직자만
const searchParams = new URLSearchParams();
searchParams.set('per_page', '100'); // API 최대 500, 드롭다운용 100 충분
searchParams.set('status', 'active'); // 재직자만
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees?${searchParams.toString()}`;
const url = `${API_URL}/v1/employees?${searchParams.toString()}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.error('[AttendanceActions] GET employees error:', response.status);
return [];
}
const result: ApiResponse<PaginatedResponse<EmployeeApiData>> = await response.json();
if (!result.success || !result.data?.data) {
console.warn('[AttendanceActions] No employees data');
return [];
}
// API는 TenantUserProfile을 반환하지만, Attendance.user_id는 User.id를 참조
// 따라서 user.id를 사용해야 함
return result.data.data.map((emp) => ({
id: String(emp.user?.id || emp.user_id), // User.id 사용
name: emp.user?.name || emp.name,
department: emp.department?.name || emp.tenant_user_profile?.department?.name || '',
position: emp.position_key || emp.tenant_user_profile?.position?.name || '',
rank: emp.tenant_user_profile?.rank || '',
}));
} catch (error) {
console.error('[AttendanceActions] getEmployeesForAttendance error:', error);
if (error || !response) {
console.error('[AttendanceActions] GET employees error:', error?.message);
return [];
}
const result: ApiResponse<PaginatedResponse<EmployeeApiData>> = await response.json();
if (!result.success || !result.data?.data) {
console.warn('[AttendanceActions] No employees data');
return [];
}
// API는 TenantUserProfile을 반환하지만, Attendance.user_id는 User.id를 참조
// 따라서 user.id를 사용해야 함
return result.data.data.map((emp) => ({
id: String(emp.user?.id || emp.user_id), // User.id 사용
name: emp.user?.name || emp.name,
department: emp.department?.name || emp.tenant_user_profile?.department?.name || '',
position: emp.position_key || emp.tenant_user_profile?.position?.name || '',
rank: emp.tenant_user_profile?.rank || '',
}));
}
// ============================================
@@ -232,86 +210,62 @@ export async function getAttendances(params?: {
sort_by?: string;
sort_dir?: 'asc' | 'desc';
}): Promise<{ data: AttendanceRecord[]; total: number; lastPage: number }> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
if (params?.user_id) searchParams.set('user_id', params.user_id);
if (params?.date) searchParams.set('date', params.date);
if (params?.date_from) searchParams.set('date_from', params.date_from);
if (params?.date_to) searchParams.set('date_to', params.date_to);
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.department_id) searchParams.set('department_id', params.department_id);
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
if (params?.user_id) searchParams.set('user_id', params.user_id);
if (params?.date) searchParams.set('date', params.date);
if (params?.date_from) searchParams.set('date_from', params.date_from);
if (params?.date_to) searchParams.set('date_to', params.date_to);
if (params?.status && params.status !== 'all') {
searchParams.set('status', params.status);
}
if (params?.department_id) searchParams.set('department_id', params.department_id);
if (params?.sort_by) searchParams.set('sort_by', params.sort_by);
if (params?.sort_dir) searchParams.set('sort_dir', params.sort_dir);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances?${searchParams.toString()}`;
const url = `${API_URL}/v1/attendances?${searchParams.toString()}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.error('[AttendanceActions] GET list error:', response.status);
return { data: [], total: 0, lastPage: 1 };
}
const result: ApiResponse<PaginatedResponse<AttendanceApiData>> = await response.json();
if (!result.success || !result.data?.data) {
console.warn('[AttendanceActions] No data in response');
return { data: [], total: 0, lastPage: 1 };
}
return {
data: result.data.data.map(transformApiToFrontend),
total: result.data.total,
lastPage: result.data.last_page,
};
} catch (error) {
console.error('[AttendanceActions] getAttendances error:', error);
if (error || !response) {
console.error('[AttendanceActions] GET list error:', error?.message);
return { data: [], total: 0, lastPage: 1 };
}
const result: ApiResponse<PaginatedResponse<AttendanceApiData>> = await response.json();
if (!result.success || !result.data?.data) {
console.warn('[AttendanceActions] No data in response');
return { data: [], total: 0, lastPage: 1 };
}
return {
data: result.data.data.map(transformApiToFrontend),
total: result.data.total,
lastPage: result.data.last_page,
};
}
/**
* 근태 상세 조회
*/
export async function getAttendanceById(id: string): Promise<AttendanceRecord | null> {
try {
const headers = await getApiHeaders();
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/${id}`, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.error('[AttendanceActions] GET attendance error:', response.status);
return null;
}
const result: ApiResponse<AttendanceApiData> = await response.json();
if (!result.success || !result.data) {
return null;
}
return transformApiToFrontend(result.data);
} catch (error) {
console.error('[AttendanceActions] getAttendanceById error:', error);
if (error || !response) {
console.error('[AttendanceActions] GET attendance error:', error?.message);
return null;
}
const result: ApiResponse<AttendanceApiData> = await response.json();
if (!result.success || !result.data) {
return null;
}
return transformApiToFrontend(result.data);
}
/**
@@ -320,42 +274,32 @@ export async function getAttendanceById(id: string): Promise<AttendanceRecord |
export async function createAttendance(
data: AttendanceFormData
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[AttendanceActions] POST attendance request:', apiData);
console.log('[AttendanceActions] POST attendance request:', apiData);
const { response, error } = await serverFetch(`${API_URL}/v1/attendances`, {
method: 'POST',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
if (error || !response) {
return { success: false, error: error?.message || '근태 등록에 실패했습니다.' };
}
const result = await response.json();
console.log('[AttendanceActions] POST attendance response:', result);
const result = await response.json();
console.log('[AttendanceActions] POST attendance response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '근태 등록에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[AttendanceActions] createAttendance error:', error);
if (!result.success) {
return {
success: false,
error: '서버 오류가 발생했습니다.',
error: result.message || '근태 등록에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
/**
@@ -365,113 +309,81 @@ export async function updateAttendance(
id: string,
data: AttendanceFormData
): Promise<{ success: boolean; data?: AttendanceRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
console.log('[AttendanceActions] PATCH attendance request:', apiData);
console.log('[AttendanceActions] PATCH attendance request:', apiData);
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/${id}`, {
method: 'PATCH',
body: JSON.stringify(apiData),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/${id}`,
{
method: 'PATCH',
headers,
body: JSON.stringify(apiData),
}
);
if (error || !response) {
return { success: false, error: error?.message || '근태 수정에 실패했습니다.' };
}
const result = await response.json();
console.log('[AttendanceActions] PATCH attendance response:', result);
const result = await response.json();
console.log('[AttendanceActions] PATCH attendance response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '근태 수정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[AttendanceActions] updateAttendance error:', error);
if (!result.success) {
return {
success: false,
error: '서버 오류가 발생했습니다.',
error: result.message || '근태 수정에 실패했습니다.',
};
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
/**
* 근태 삭제
*/
export async function deleteAttendance(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/${id}`, { method: 'DELETE' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/${id}`,
{
method: 'DELETE',
headers,
}
);
if (error || !response) {
return { success: false, error: error?.message || '근태 삭제에 실패했습니다.' };
}
const result = await response.json();
console.log('[AttendanceActions] DELETE attendance response:', result);
const result = await response.json();
console.log('[AttendanceActions] DELETE attendance response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '근태 삭제에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[AttendanceActions] deleteAttendance error:', error);
if (!result.success) {
return {
success: false,
error: '서버 오류가 발생했습니다.',
error: result.message || '근태 삭제에 실패했습니다.',
};
}
return { success: true };
}
/**
* 근태 일괄 삭제
*/
export async function deleteAttendances(ids: string[]): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const { response, error } = await serverFetch(`${API_URL}/v1/attendances/bulk-delete`, {
method: 'POST',
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/bulk-delete`,
{
method: 'POST',
headers,
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
}
);
if (error || !response) {
return { success: false, error: error?.message || '근태 일괄 삭제에 실패했습니다.' };
}
const result = await response.json();
console.log('[AttendanceActions] BULK DELETE attendance response:', result);
const result = await response.json();
console.log('[AttendanceActions] BULK DELETE attendance response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '근태 일괄 삭제에 실패했습니다.',
};
}
return { success: true };
} catch (error) {
console.error('[AttendanceActions] deleteAttendances error:', error);
if (!result.success) {
return {
success: false,
error: '서버 오류가 발생했습니다.',
error: result.message || '근태 일괄 삭제에 실패했습니다.',
};
}
return { success: true };
}
/**
@@ -482,43 +394,33 @@ export async function getMonthlyStats(params: {
month: number;
user_id?: string;
}): Promise<AttendanceStats | null> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
searchParams.set('year', String(params.year));
searchParams.set('month', String(params.month));
if (params.user_id) searchParams.set('user_id', params.user_id);
searchParams.set('year', String(params.year));
searchParams.set('month', String(params.month));
if (params.user_id) searchParams.set('user_id', params.user_id);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/attendances/monthly-stats?${searchParams.toString()}`;
const url = `${API_URL}/v1/attendances/monthly-stats?${searchParams.toString()}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.error('[AttendanceActions] GET monthly-stats error:', response.status);
return null;
}
const result = await response.json();
if (!result.success || !result.data) {
return null;
}
return {
year: result.data.year,
month: result.data.month,
totalDays: result.data.total_days,
byStatus: result.data.by_status,
totalWorkMinutes: result.data.total_work_minutes,
totalOvertimeMinutes: result.data.total_overtime_minutes,
};
} catch (error) {
console.error('[AttendanceActions] getMonthlyStats error:', error);
if (error || !response) {
console.error('[AttendanceActions] GET monthly-stats error:', error?.message);
return null;
}
const result = await response.json();
if (!result.success || !result.data) {
return null;
}
return {
year: result.data.year,
month: result.data.month,
totalDays: result.data.total_days,
byStatus: result.data.by_status,
totalWorkMinutes: result.data.total_work_minutes,
totalOvertimeMinutes: result.data.total_overtime_minutes,
};
}

View File

@@ -1,6 +1,6 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { Card, CardFormData, CardStatus } from './types';
// API 응답 타입
@@ -54,19 +54,6 @@ interface CardResponse {
data: CardApiData;
}
// API 헤더 생성
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// API URL (without double /api)
const API_URL = `${process.env.NEXT_PUBLIC_API_URL || process.env.API_URL}/api`;
@@ -147,153 +134,128 @@ export async function getCards(params?: {
page?: number;
per_page?: number;
}): Promise<{ success: boolean; data?: Card[]; pagination?: { total: number; currentPage: number; lastPage: number }; error?: string }> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.search) searchParams.set('search', params.search);
if (params?.status && params.status !== 'all') searchParams.set('status', mapFrontendStatusToApi(params.status as CardStatus));
if (params?.page) searchParams.set('page', String(params.page));
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
if (params?.search) searchParams.set('search', params.search);
if (params?.status && params.status !== 'all') searchParams.set('status', mapFrontendStatusToApi(params.status as CardStatus));
if (params?.page) searchParams.set('page', String(params.page));
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
const url = `${API_URL}/v1/cards${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const url = `${API_URL}/v1/cards${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const result: CardListResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '카드 목록을 불러오는데 실패했습니다.' };
}
return {
success: true,
data: result.data.data.map(transformApiToFrontend),
pagination: {
total: result.data.total,
currentPage: result.data.current_page,
lastPage: result.data.last_page,
},
};
} catch (error) {
console.error('[getCards] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '카드 목록을 불러오는데 실패했습니다.' };
}
const result: CardListResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '카드 목록을 불러오는데 실패했습니다.' };
}
return {
success: true,
data: result.data.data.map(transformApiToFrontend),
pagination: {
total: result.data.total,
currentPage: result.data.current_page,
lastPage: result.data.last_page,
},
};
}
/**
* 카드 상세 조회
*/
export async function getCard(id: string): Promise<{ success: boolean; data?: Card; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/cards/${id}`, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}`, { method: 'GET' });
const result: CardResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '카드 정보를 불러오는데 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[getCard] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '카드 정보를 불러오는데 실패했습니다.' };
}
const result: CardResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '카드 정보를 불러오는데 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
/**
* 카드 등록
*/
export async function createCard(data: CardFormData): Promise<{ success: boolean; data?: Card; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(`${API_URL}/v1/cards`, {
method: 'POST',
body: JSON.stringify(apiData),
});
const response = await fetch(`${API_URL}/v1/cards`, {
method: 'POST',
headers,
body: JSON.stringify(apiData),
});
const result: CardResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '카드 등록에 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[createCard] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '카드 등록에 실패했습니다.' };
}
const result: CardResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '카드 등록에 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
/**
* 카드 수정
*/
export async function updateCard(id: string, data: CardFormData): Promise<{ success: boolean; data?: Card; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}`, {
method: 'PUT',
body: JSON.stringify(apiData),
});
const response = await fetch(`${API_URL}/v1/cards/${id}`, {
method: 'PUT',
headers,
body: JSON.stringify(apiData),
});
const result: CardResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '카드 수정에 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[updateCard] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '카드 수정에 실패했습니다.' };
}
const result: CardResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '카드 수정에 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
/**
* 카드 삭제
*/
export async function deleteCard(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/cards/${id}`, {
method: 'DELETE',
headers,
});
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}`, { method: 'DELETE' });
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '카드 삭제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
console.error('[deleteCard] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '카드 삭제에 실패했습니다.' };
}
const result = await response.json();
if (!result.success) {
return { success: false, error: result.message || '카드 삭제에 실패했습니다.' };
}
return { success: true };
}
/**
@@ -319,27 +281,22 @@ export async function deleteCards(ids: string[]): Promise<{ success: boolean; er
* 카드 상태 토글
*/
export async function toggleCardStatus(id: string): Promise<{ success: boolean; data?: Card; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/cards/${id}/toggle`, {
method: 'PATCH',
headers,
});
const { response, error } = await serverFetch(`${API_URL}/v1/cards/${id}/toggle`, { method: 'PATCH' });
const result: CardResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[toggleCardStatus] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '상태 변경에 실패했습니다.' };
}
const result: CardResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
/**
@@ -347,36 +304,30 @@ export async function toggleCardStatus(id: string): Promise<{ success: boolean;
* 주의: Card.assigned_user_id는 User.id를 참조하므로 user.id를 반환해야 함
*/
export async function getActiveEmployees(): Promise<{ success: boolean; data?: Array<{ id: string; label: string }>; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/employees?status=active&per_page=50`, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(`${API_URL}/v1/employees?status=active&per_page=50`, { method: 'GET' });
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '직원 목록을 불러오는데 실패했습니다.' };
}
// API는 TenantUserProfile을 반환하지만, Card.assigned_user_id는 User.id를 참조
// 따라서 user.id를 사용해야 함
const employees = result.data.data.map((emp: {
id: number;
user_id: number;
user?: { id: number; name: string };
department?: { name: string };
position_key?: string;
}) => ({
id: String(emp.user?.id || emp.user_id), // User.id 사용
label: `${emp.department?.name || ''} / ${emp.user?.name || ''} / ${emp.position_key || ''}`.replace(/^ \/ | \/ $/g, '').replace(/ \/ $/g, ''),
}));
return { success: true, data: employees };
} catch (error) {
console.error('[getActiveEmployees] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '직원 목록을 불러오는데 실패했습니다.' };
}
const result = await response.json();
if (!result.success) {
return { success: false, error: result.message || '직원 목록을 불러오는데 실패했습니다.' };
}
// API는 TenantUserProfile을 반환하지만, Card.assigned_user_id는 User.id를 참조
// 따라서 user.id를 사용해야 함
const employees = result.data.data.map((emp: {
id: number;
user_id: number;
user?: { id: number; name: string };
department?: { name: string };
position_key?: string;
}) => ({
id: String(emp.user?.id || emp.user_id), // User.id 사용
label: `${emp.department?.name || ''} / ${emp.user?.name || ''} / ${emp.position_key || ''}`.replace(/^ \/ | \/ $/g, '').replace(/ \/ $/g, ''),
}));
return { success: true, data: employees };
}

View File

@@ -12,7 +12,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
// ============================================
// 타입 정의
@@ -91,20 +91,8 @@ interface ApiResponse<T> {
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// API URL
const API_URL = `${process.env.NEXT_PUBLIC_API_URL}/api`;
/**
* API 응답을 프론트엔드 형식으로 변환 (재귀)
@@ -141,43 +129,35 @@ function transformApiToFrontend(apiData: ApiDepartment, depth: number = 0): Depa
export async function getDepartmentTree(params?: {
withUsers?: boolean;
}): Promise<{ success: boolean; data?: DepartmentRecord[]; error?: string }> {
try {
const headers = await getApiHeaders();
const queryParams = new URLSearchParams();
const queryParams = new URLSearchParams();
if (params?.withUsers) {
queryParams.append('with_users', '1');
}
if (params?.withUsers) {
queryParams.append('with_users', '1');
}
const queryString = queryParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/tree${queryString ? `?${queryString}` : ''}`;
const queryString = queryParams.toString();
const url = `${API_URL}/v1/departments/tree${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
});
const { response, error } = await serverFetch(url, { method: 'GET' });
const result: ApiResponse<ApiDepartment[]> = await response.json();
if (error || !response) {
return { success: false, error: error?.message || '부서 트리 조회에 실패했습니다.' };
}
if (result.success && result.data) {
const transformed = result.data.map((dept) => transformApiToFrontend(dept, 0));
return {
success: true,
data: transformed,
};
}
const result: ApiResponse<ApiDepartment[]> = await response.json();
if (result.success && result.data) {
const transformed = result.data.map((dept) => transformApiToFrontend(dept, 0));
return {
success: false,
error: result.message || '부서 트리 조회에 실패했습니다.',
};
} catch (error) {
console.error('[getDepartmentTree] Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '부서 트리 조회에 실패했습니다.',
success: true,
data: transformed,
};
}
return {
success: false,
error: result.message || '부서 트리 조회에 실패했습니다.',
};
}
/**
@@ -187,33 +167,25 @@ export async function getDepartmentTree(params?: {
export async function getDepartmentById(
id: number
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/${id}`, {
method: 'GET',
headers,
});
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, { method: 'GET' });
const result: ApiResponse<ApiDepartment> = await response.json();
if (error || !response) {
return { success: false, error: error?.message || '부서 조회에 실패했습니다.' };
}
if (result.success && result.data) {
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
const result: ApiResponse<ApiDepartment> = await response.json();
if (result.success && result.data) {
return {
success: false,
error: result.message || '부서 조회에 실패했습니다.',
};
} catch (error) {
console.error('[getDepartmentById] Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '부서 조회에 실패했습니다.',
success: true,
data: transformApiToFrontend(result.data),
};
}
return {
success: false,
error: result.message || '부서 조회에 실패했습니다.',
};
}
/**
@@ -223,41 +195,35 @@ export async function getDepartmentById(
export async function createDepartment(
data: CreateDepartmentRequest
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments`, {
method: 'POST',
headers,
body: JSON.stringify({
parent_id: data.parentId,
code: data.code,
name: data.name,
description: data.description,
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
sort_order: data.sortOrder,
}),
});
const { response, error } = await serverFetch(`${API_URL}/v1/departments`, {
method: 'POST',
body: JSON.stringify({
parent_id: data.parentId,
code: data.code,
name: data.name,
description: data.description,
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
sort_order: data.sortOrder,
}),
});
const result: ApiResponse<ApiDepartment> = await response.json();
if (error || !response) {
return { success: false, error: error?.message || '부서 생성에 실패했습니다.' };
}
if (result.success && result.data) {
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
const result: ApiResponse<ApiDepartment> = await response.json();
if (result.success && result.data) {
return {
success: false,
error: result.message || '부서 생성에 실패했습니다.',
};
} catch (error) {
console.error('[createDepartment] Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '부서 생성에 실패했습니다.',
success: true,
data: transformApiToFrontend(result.data),
};
}
return {
success: false,
error: result.message || '부서 생성에 실패했습니다.',
};
}
/**
@@ -268,41 +234,35 @@ export async function updateDepartment(
id: number,
data: UpdateDepartmentRequest
): Promise<{ success: boolean; data?: DepartmentRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/${id}`, {
method: 'PATCH',
headers,
body: JSON.stringify({
parent_id: data.parentId === null ? 0 : data.parentId, // null이면 0(최상위)으로 전환
code: data.code,
name: data.name,
description: data.description,
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
sort_order: data.sortOrder,
}),
});
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, {
method: 'PATCH',
body: JSON.stringify({
parent_id: data.parentId === null ? 0 : data.parentId, // null이면 0(최상위)으로 전환
code: data.code,
name: data.name,
description: data.description,
is_active: data.isActive !== undefined ? (data.isActive ? 1 : 0) : undefined,
sort_order: data.sortOrder,
}),
});
const result: ApiResponse<ApiDepartment> = await response.json();
if (error || !response) {
return { success: false, error: error?.message || '부서 수정에 실패했습니다.' };
}
if (result.success && result.data) {
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
const result: ApiResponse<ApiDepartment> = await response.json();
if (result.success && result.data) {
return {
success: false,
error: result.message || '부서 수정에 실패했습니다.',
};
} catch (error) {
console.error('[updateDepartment] Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '부서 수정에 실패했습니다.',
success: true,
data: transformApiToFrontend(result.data),
};
}
return {
success: false,
error: result.message || '부서 수정에 실패했습니다.',
};
}
/**
@@ -312,30 +272,22 @@ export async function updateDepartment(
export async function deleteDepartment(
id: number
): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/${id}`, {
method: 'DELETE',
headers,
});
const { response, error } = await serverFetch(`${API_URL}/v1/departments/${id}`, { method: 'DELETE' });
const result: ApiResponse<{ id: number; deleted_at: string }> = await response.json();
if (result.success) {
return { success: true };
}
return {
success: false,
error: result.message || '부서 삭제에 실패했습니다.',
};
} catch (error) {
console.error('[deleteDepartment] Error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '부서 삭제에 실패했습니다.',
};
if (error || !response) {
return { success: false, error: error?.message || '부서 삭제에 실패했습니다.' };
}
const result: ApiResponse<{ id: number; deleted_at: string }> = await response.json();
if (result.success) {
return { success: true };
}
return {
success: false,
error: result.message || '부서 삭제에 실패했습니다.',
};
}
/**

View File

@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react';
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
import { useRouter } from 'next/navigation';
import { useRouter, useParams } from 'next/navigation';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -16,7 +16,8 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Users, Plus, Trash2, ArrowLeft, Save, Settings, Camera } from 'lucide-react';
import { Users, Plus, Trash2, ArrowLeft, Save, Settings, Camera, Edit } from 'lucide-react';
import { toast } from 'sonner';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { FieldSettingsDialog } from './FieldSettingsDialog';
import type {
@@ -36,12 +37,23 @@ import {
import { uploadProfileImage } from './actions';
interface EmployeeFormProps {
mode: 'create' | 'edit';
mode: 'create' | 'edit' | 'view';
employee?: Employee;
onSave: (data: EmployeeFormData) => void;
onSave?: (data: EmployeeFormData) => void;
onEdit?: () => void;
onDelete?: () => void;
fieldSettings?: FieldSettings;
}
// 유효성 검사 에러 타입
interface ValidationErrors {
name?: string;
email?: string;
userId?: string;
password?: string;
confirmPassword?: string;
}
const initialFormData: EmployeeFormData = {
name: '',
residentNumber: '',
@@ -74,10 +86,16 @@ export function EmployeeForm({
mode,
employee,
onSave,
onEdit,
onDelete,
fieldSettings: initialFieldSettings = DEFAULT_FIELD_SETTINGS,
}: EmployeeFormProps) {
const router = useRouter();
const params = useParams();
const locale = params.locale as string || 'ko';
const [formData, setFormData] = useState<EmployeeFormData>(initialFormData);
const [errors, setErrors] = useState<ValidationErrors>({});
const isViewMode = mode === 'view';
// Daum 우편번호 서비스
const { openPostcode } = useDaumPostcode({
@@ -97,7 +115,12 @@ export function EmployeeForm({
const [showFieldSettings, setShowFieldSettings] = useState(false);
const [fieldSettings, setFieldSettings] = useState<FieldSettings>(initialFieldSettings);
const title = mode === 'create' ? '사원 등록' : '사원 수정';
const title = mode === 'create' ? '사원 등록' : mode === 'edit' ? '사원 수정' : '사원 상세';
const description = mode === 'create'
? '새로운 사원 정보를 입력합니다'
: mode === 'edit'
? '사원 정보를 수정합니다'
: '사원 정보를 확인합니다';
// localStorage에서 항목 설정 로드
useEffect(() => {
@@ -117,9 +140,9 @@ export function EmployeeForm({
localStorage.setItem('employeeFieldSettings', JSON.stringify(newSettings));
};
// 데이터 초기화
// 데이터 초기화 (edit, view 모드)
useEffect(() => {
if (employee && mode === 'edit') {
if (employee && (mode === 'edit' || mode === 'view')) {
setFormData({
name: employee.name,
residentNumber: employee.residentNumber || '',
@@ -153,6 +176,62 @@ export function EmployeeForm({
// 입력 변경 핸들러
const handleChange = (field: keyof EmployeeFormData, value: unknown) => {
setFormData(prev => ({ ...prev, [field]: value }));
// 에러 초기화
if (errors[field as keyof ValidationErrors]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
// 이메일 형식 검사
const isValidEmail = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
// 유효성 검사
const validateForm = (): boolean => {
const newErrors: ValidationErrors = {};
// 이름 필수
if (!formData.name.trim()) {
newErrors.name = '이름을 입력해주세요.';
}
// 이메일 필수 + 형식 검사
if (!formData.email.trim()) {
newErrors.email = '이메일을 입력해주세요.';
} else if (!isValidEmail(formData.email)) {
newErrors.email = '올바른 이메일 형식이 아닙니다.';
}
// 아이디 필수
if (!formData.userId.trim()) {
newErrors.userId = '아이디를 입력해주세요.';
}
// 등록 모드일 때 비밀번호 검사
if (mode === 'create') {
if (!formData.password) {
newErrors.password = '비밀번호를 입력해주세요.';
} else if (formData.password.length < 6) {
newErrors.password = '비밀번호는 6자 이상이어야 합니다.';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = '비밀번호가 일치하지 않습니다.';
}
}
setErrors(newErrors);
// 에러가 있으면 첫 번째 에러 메시지 표시
const firstError = Object.values(newErrors)[0];
if (firstError) {
toast.error(firstError);
return false;
}
return true;
};
// 부서/직책 추가
@@ -191,12 +270,21 @@ export function EmployeeForm({
// 저장
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(formData);
// view 모드에서는 저장 불가
if (isViewMode) return;
// 유효성 검사
if (!validateForm()) {
return;
}
onSave?.(formData);
};
// 취소
// 취소 (목록으로 이동)
const handleCancel = () => {
router.back();
router.push(`/${locale}/hr/employee-management`);
};
return (
@@ -205,17 +293,19 @@ export function EmployeeForm({
<div className="flex items-start justify-between mb-6">
<PageHeader
title={title}
description={mode === 'create' ? '새로운 사원 정보를 입력합니다' : '사원 정보를 수정합니다'}
description={description}
icon={Users}
/>
<Button
type="button"
variant="outline"
onClick={() => setShowFieldSettings(true)}
>
<Settings className="w-4 h-4 mr-2" />
</Button>
{!isViewMode && (
<Button
type="button"
variant="outline"
onClick={() => setShowFieldSettings(true)}
>
<Settings className="w-4 h-4 mr-2" />
</Button>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-6">
@@ -234,8 +324,10 @@ export function EmployeeForm({
value={formData.name}
onChange={(e) => handleChange('name', e.target.value)}
placeholder="이름을 입력하세요"
required
disabled={isViewMode}
className={errors.name ? 'border-red-500' : ''}
/>
{errors.name && <p className="text-sm text-red-500">{errors.name}</p>}
</div>
<div className="space-y-2">
@@ -245,6 +337,7 @@ export function EmployeeForm({
value={formData.residentNumber}
onChange={(e) => handleChange('residentNumber', e.target.value)}
placeholder="000000-0000000"
disabled={isViewMode}
/>
</div>
@@ -255,18 +348,22 @@ export function EmployeeForm({
value={formData.phone}
onChange={(e) => handleChange('phone', e.target.value)}
placeholder="010-0000-0000"
disabled={isViewMode}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Label htmlFor="email"> *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="email@company.com"
disabled={isViewMode}
className={errors.email ? 'border-red-500' : ''}
/>
{errors.email && <p className="text-sm text-red-500">{errors.email}</p>}
</div>
<div className="space-y-2">
@@ -277,6 +374,7 @@ export function EmployeeForm({
value={formData.salary}
onChange={(e) => handleChange('salary', e.target.value)}
placeholder="연봉 (원)"
disabled={isViewMode}
/>
</div>
</div>
@@ -289,16 +387,19 @@ export function EmployeeForm({
value={formData.bankAccount.bankName}
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, bankName: e.target.value })}
placeholder="은행명"
disabled={isViewMode}
/>
<Input
value={formData.bankAccount.accountNumber}
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, accountNumber: e.target.value })}
placeholder="계좌번호"
disabled={isViewMode}
/>
<Input
value={formData.bankAccount.accountHolder}
onChange={(e) => handleChange('bankAccount', { ...formData.bankAccount, accountHolder: e.target.value })}
placeholder="예금주"
disabled={isViewMode}
/>
</div>
</div>
@@ -318,32 +419,46 @@ export function EmployeeForm({
{fieldSettings.showProfileImage && (
<div className="space-y-2 flex-shrink-0">
<Label> </Label>
<div className="w-32 h-32 border border-dashed border-gray-300 rounded-md flex flex-col items-center justify-center bg-gray-50 relative cursor-pointer hover:bg-gray-100">
<span className="text-sm text-gray-400 mb-1">IMG</span>
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
<Camera className="w-4 h-4 text-gray-500" />
</div>
<input
type="file"
accept="image/png,image/jpeg,image/gif"
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
// 미리보기 즉시 표시
handleChange('profileImage', URL.createObjectURL(file));
// 서버에 업로드
const result = await uploadProfileImage(file);
if (result.success && result.data?.url) {
handleChange('profileImage', result.data.url);
<div className={`w-32 h-32 border border-dashed border-gray-300 rounded-md flex flex-col items-center justify-center bg-gray-50 relative ${isViewMode ? '' : 'cursor-pointer hover:bg-gray-100'}`}>
{formData.profileImage ? (
<img
src={formData.profileImage}
alt="프로필"
className="w-full h-full object-cover rounded-md"
/>
) : (
<>
<span className="text-sm text-gray-400 mb-1">IMG</span>
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
<Camera className="w-4 h-4 text-gray-500" />
</div>
</>
)}
{!isViewMode && (
<input
type="file"
accept="image/png,image/jpeg,image/gif"
className="absolute inset-0 opacity-0 cursor-pointer"
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
// 미리보기 즉시 표시
handleChange('profileImage', URL.createObjectURL(file));
// 서버에 업로드
const result = await uploadProfileImage(file);
if (result.success && result.data?.url) {
handleChange('profileImage', result.data.url);
}
}
}
}}
/>
}}
/>
)}
</div>
<p className="text-xs text-gray-500">
1 250 X 250px, 10MB <br />PNG, JPEG, GIF
</p>
{!isViewMode && (
<p className="text-xs text-gray-500">
1 250 X 250px, 10MB <br />PNG, JPEG, GIF
</p>
)}
</div>
)}
@@ -358,6 +473,7 @@ export function EmployeeForm({
value={formData.employeeCode}
onChange={(e) => handleChange('employeeCode', e.target.value)}
placeholder="사원코드를 입력해주세요"
disabled={isViewMode}
/>
</div>
)}
@@ -369,13 +485,14 @@ export function EmployeeForm({
value={formData.gender}
onValueChange={(value) => handleChange('gender', value)}
className="flex items-center gap-4 h-10"
disabled={isViewMode}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="male" id="gender-male" />
<RadioGroupItem value="male" id="gender-male" disabled={isViewMode} />
<Label htmlFor="gender-male" className="font-normal cursor-pointer"></Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="female" id="gender-female" />
<RadioGroupItem value="female" id="gender-female" disabled={isViewMode} />
<Label htmlFor="gender-female" className="font-normal cursor-pointer"></Label>
</div>
</RadioGroup>
@@ -388,21 +505,25 @@ export function EmployeeForm({
<div className="space-y-2">
<Label></Label>
<div className="flex gap-2">
<Button type="button" variant="default" size="sm" onClick={openPostcode} className="bg-blue-500 hover:bg-blue-600">
</Button>
{!isViewMode && (
<Button type="button" variant="default" size="sm" onClick={openPostcode} className="bg-blue-500 hover:bg-blue-600">
</Button>
)}
<Input
value={formData.address.zipCode}
onChange={(e) => handleChange('address', { ...formData.address, zipCode: e.target.value })}
placeholder=""
className="w-24"
readOnly
disabled={isViewMode}
/>
<Input
value={formData.address.address2}
onChange={(e) => handleChange('address', { ...formData.address, address2: e.target.value })}
placeholder="상세주소를 입력해주세요"
className="flex-1"
disabled={isViewMode}
/>
</div>
</div>
@@ -429,6 +550,7 @@ export function EmployeeForm({
type="date"
value={formData.hireDate}
onChange={(e) => handleChange('hireDate', e.target.value)}
disabled={isViewMode}
/>
</div>
)}
@@ -439,8 +561,9 @@ export function EmployeeForm({
<Select
value={formData.employmentType}
onValueChange={(value) => handleChange('employmentType', value)}
disabled={isViewMode}
>
<SelectTrigger>
<SelectTrigger disabled={isViewMode}>
<SelectValue placeholder="고용형태 선택" />
</SelectTrigger>
<SelectContent>
@@ -460,6 +583,7 @@ export function EmployeeForm({
value={formData.rank}
onChange={(e) => handleChange('rank', e.target.value)}
placeholder="직급 입력"
disabled={isViewMode}
/>
</div>
)}
@@ -470,8 +594,9 @@ export function EmployeeForm({
<Select
value={formData.status}
onValueChange={(value) => handleChange('status', value)}
disabled={isViewMode}
>
<SelectTrigger>
<SelectTrigger disabled={isViewMode}>
<SelectValue placeholder="상태 선택" />
</SelectTrigger>
<SelectContent>
@@ -489,20 +614,22 @@ export function EmployeeForm({
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>/</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddDepartmentPosition}
>
<Plus className="w-4 h-4 mr-1" />
</Button>
{!isViewMode && (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddDepartmentPosition}
>
<Plus className="w-4 h-4 mr-1" />
</Button>
)}
</div>
{formData.departmentPositions.length === 0 ? (
<p className="text-sm text-muted-foreground py-4 text-center border rounded-md">
/
{isViewMode ? '등록된 부서/직책이 없습니다' : '부서/직책을 추가해주세요'}
</p>
) : (
<div className="space-y-2">
@@ -513,21 +640,25 @@ export function EmployeeForm({
onChange={(e) => handleDepartmentPositionChange(dp.id, 'departmentName', e.target.value)}
placeholder="부서명"
className="flex-1"
disabled={isViewMode}
/>
<Input
value={dp.positionName}
onChange={(e) => handleDepartmentPositionChange(dp.id, 'positionName', e.target.value)}
placeholder="직책"
className="flex-1"
disabled={isViewMode}
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveDepartmentPosition(dp.id)}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
{!isViewMode && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleRemoveDepartmentPosition(dp.id)}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
)}
</div>
))}
</div>
@@ -543,8 +674,9 @@ export function EmployeeForm({
<Select
value={formData.clockInLocation}
onValueChange={(value) => handleChange('clockInLocation', value)}
disabled={isViewMode}
>
<SelectTrigger>
<SelectTrigger disabled={isViewMode}>
<SelectValue placeholder="출근 위치 선택" />
</SelectTrigger>
<SelectContent>
@@ -563,8 +695,9 @@ export function EmployeeForm({
<Select
value={formData.clockOutLocation}
onValueChange={(value) => handleChange('clockOutLocation', value)}
disabled={isViewMode}
>
<SelectTrigger>
<SelectTrigger disabled={isViewMode}>
<SelectValue placeholder="퇴근 위치 선택" />
</SelectTrigger>
<SelectContent>
@@ -589,6 +722,7 @@ export function EmployeeForm({
type="date"
value={formData.resignationDate}
onChange={(e) => handleChange('resignationDate', e.target.value)}
disabled={isViewMode}
/>
</div>
)}
@@ -601,6 +735,7 @@ export function EmployeeForm({
value={formData.resignationReason}
onChange={(e) => handleChange('resignationReason', e.target.value)}
placeholder="퇴직 사유를 입력하세요"
disabled={isViewMode}
/>
</div>
)}
@@ -625,7 +760,10 @@ export function EmployeeForm({
value={formData.userId}
onChange={(e) => handleChange('userId', e.target.value)}
placeholder="사용자 아이디"
disabled={isViewMode}
className={errors.userId ? 'border-red-500' : ''}
/>
{errors.userId && <p className="text-sm text-red-500">{errors.userId}</p>}
</div>
{mode === 'create' && (
@@ -638,18 +776,22 @@ export function EmployeeForm({
value={formData.password}
onChange={(e) => handleChange('password', e.target.value)}
placeholder="비밀번호"
className={errors.password ? 'border-red-500' : ''}
/>
{errors.password && <p className="text-sm text-red-500">{errors.password}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword"> </Label>
<Label htmlFor="confirmPassword"> *</Label>
<Input
id="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={(e) => handleChange('confirmPassword', e.target.value)}
placeholder="비밀번호 확인"
className={errors.confirmPassword ? 'border-red-500' : ''}
/>
{errors.confirmPassword && <p className="text-sm text-red-500">{errors.confirmPassword}</p>}
</div>
</>
)}
@@ -659,8 +801,9 @@ export function EmployeeForm({
<Select
value={formData.role}
onValueChange={(value) => handleChange('role', value)}
disabled={isViewMode}
>
<SelectTrigger>
<SelectTrigger disabled={isViewMode}>
<SelectValue placeholder="권한 선택" />
</SelectTrigger>
<SelectContent>
@@ -676,8 +819,9 @@ export function EmployeeForm({
<Select
value={formData.accountStatus}
onValueChange={(value) => handleChange('accountStatus', value)}
disabled={isViewMode}
>
<SelectTrigger>
<SelectTrigger disabled={isViewMode}>
<SelectValue placeholder="상태 선택" />
</SelectTrigger>
<SelectContent>
@@ -695,12 +839,25 @@ export function EmployeeForm({
<div className="flex items-center justify-between">
<Button type="button" variant="outline" onClick={handleCancel}>
<ArrowLeft className="w-4 h-4 mr-2" />
</Button>
<Button type="submit">
<Save className="w-4 h-4 mr-2" />
{mode === 'create' ? '등록' : '저장'}
</Button>
{isViewMode ? (
<div className="flex gap-2">
<Button type="button" onClick={onEdit}>
<Edit className="w-4 h-4 mr-2" />
</Button>
<Button type="button" variant="destructive" onClick={onDelete}>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
</div>
) : (
<Button type="submit">
<Save className="w-4 h-4 mr-2" />
{mode === 'create' ? '등록' : '저장'}
</Button>
)}
</div>
</form>

View File

@@ -9,11 +9,14 @@
* - DELETE /api/v1/employees/{id} - 삭제
* - POST /api/v1/employees/bulk-delete - 일괄 삭제
* - GET /api/v1/employees/stats - 통계
*
* 🚨 401 에러 시 __authError: true 반환 → 클라이언트에서 로그인 페이지로 리다이렉트
*/
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { Employee, EmployeeFormData, EmployeeStats } from './types';
import { transformApiToFrontend, transformFrontendToApi, type EmployeeApiData } from './utils';
@@ -35,25 +38,6 @@ interface PaginatedResponse<T> {
last_page: number;
}
// ============================================
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ============================================
// API 함수
// ============================================
@@ -70,9 +54,8 @@ export async function getEmployees(params?: {
has_account?: boolean;
sort_by?: string;
sort_dir?: 'asc' | 'desc';
}): Promise<{ data: Employee[]; total: number; lastPage: number }> {
}): Promise<{ data: Employee[]; total: number; lastPage: number; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -90,14 +73,15 @@ export async function getEmployees(params?: {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees?${searchParams.toString()}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (!response.ok) {
console.error('[EmployeeActions] GET list error:', response.status);
// 🚨 401 인증 에러 → 클라이언트에서 로그인 페이지로 리다이렉트
if (error?.__authError) {
return { data: [], total: 0, lastPage: 1, __authError: true };
}
if (!response || !response.ok) {
console.error('[EmployeeActions] GET list error:', response?.status);
return { data: [], total: 0, lastPage: 1 };
}
@@ -122,21 +106,18 @@ export async function getEmployees(params?: {
/**
* 직원 상세 조회
*/
export async function getEmployeeById(id: string): Promise<Employee | null> {
export async function getEmployeeById(id: string): Promise<Employee | null | { __authError: true }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
// 🚨 401 인증 에러
if (error?.__authError) {
return { __authError: true };
}
if (!response.ok) {
console.error('[EmployeeActions] GET employee error:', response.status);
if (!response || !response.ok) {
console.error('[EmployeeActions] GET employee error:', response?.status);
return null;
}
@@ -158,21 +139,26 @@ export async function getEmployeeById(id: string): Promise<Employee | null> {
*/
export async function createEmployee(
data: EmployeeFormData
): Promise<{ success: boolean; data?: Employee; error?: string }> {
): Promise<{ success: boolean; data?: Employee; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees`;
console.log('[EmployeeActions] POST employee request:', apiData);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(apiData),
});
// 🚨 401 인증 에러
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
}
const result = await response.json();
console.log('[EmployeeActions] POST employee response:', result);
@@ -203,21 +189,26 @@ export async function createEmployee(
export async function updateEmployee(
id: string,
data: EmployeeFormData
): Promise<{ success: boolean; data?: Employee; error?: string }> {
): Promise<{ success: boolean; data?: Employee; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`;
console.log('[EmployeeActions] PATCH employee request:', apiData);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`,
{
method: 'PATCH',
headers,
body: JSON.stringify(apiData),
}
);
const { response, error } = await serverFetch(url, {
method: 'PATCH',
body: JSON.stringify(apiData),
});
// 🚨 401 인증 에러
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
}
const result = await response.json();
console.log('[EmployeeActions] PATCH employee response:', result);
@@ -245,17 +236,19 @@ export async function updateEmployee(
/**
* 직원 삭제 (퇴직 처리)
*/
export async function deleteEmployee(id: string): Promise<{ success: boolean; error?: string }> {
export async function deleteEmployee(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`;
const { response, error } = await serverFetch(url, { method: 'DELETE' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/${id}`,
{
method: 'DELETE',
headers,
}
);
// 🚨 401 인증 에러
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
}
const result = await response.json();
console.log('[EmployeeActions] DELETE employee response:', result);
@@ -280,18 +273,22 @@ export async function deleteEmployee(id: string): Promise<{ success: boolean; er
/**
* 직원 일괄 삭제
*/
export async function deleteEmployees(ids: string[]): Promise<{ success: boolean; error?: string }> {
export async function deleteEmployees(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/bulk-delete`;
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
});
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/bulk-delete`,
{
method: 'POST',
headers,
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
}
);
// 🚨 401 인증 에러
if (error?.__authError) {
return { success: false, __authError: true };
}
if (!response) {
return { success: false, error: error?.message || '서버 오류가 발생했습니다.' };
}
const result = await response.json();
console.log('[EmployeeActions] BULK DELETE employee response:', result);
@@ -316,21 +313,18 @@ export async function deleteEmployees(ids: string[]): Promise<{ success: boolean
/**
* 직원 통계 조회
*/
export async function getEmployeeStats(): Promise<EmployeeStats | null> {
export async function getEmployeeStats(): Promise<EmployeeStats | null | { __authError: true }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/stats`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/employees/stats`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
// 🚨 401 인증 에러
if (error?.__authError) {
return { __authError: true };
}
if (!response.ok) {
console.error('[EmployeeActions] GET stats error:', response.status);
if (!response || !response.ok) {
console.error('[EmployeeActions] GET stats error:', response?.status);
return null;
}
@@ -359,11 +353,17 @@ export async function uploadProfileImage(file: File): Promise<{
success: boolean;
data?: { url: string; path: string };
error?: string;
__authError?: boolean;
}> {
try {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
// 토큰 없으면 인증 에러
if (!token) {
return { success: false, __authError: true };
}
const formData = new FormData();
formData.append('file', file);
formData.append('directory', 'employees/profiles');
@@ -373,13 +373,18 @@ export async function uploadProfileImage(file: File): Promise<{
{
method: 'POST',
headers: {
'Authorization': token ? `Bearer ${token}` : '',
'Authorization': `Bearer ${token}`,
'X-API-KEY': process.env.API_KEY || '',
},
body: formData,
}
);
// 🚨 401 인증 에러
if (response.status === 401) {
return { success: false, __authError: true };
}
if (!response.ok) {
return { success: false, error: `파일 업로드 실패: ${response.status}` };
}

View File

@@ -1,6 +1,6 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { SalaryRecord, SalaryDetail, PaymentStatus } from './types';
// API 응답 타입
@@ -85,19 +85,6 @@ interface BulkUpdateResponse {
};
}
// API 헤더 생성
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// API URL
const API_URL = `${process.env.NEXT_PUBLIC_API_URL || process.env.API_URL}/api`;
@@ -184,46 +171,40 @@ export async function getSalaries(params?: {
pagination?: { total: number; currentPage: number; lastPage: number };
error?: string
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.search) searchParams.set('search', params.search);
if (params?.year) searchParams.set('year', String(params.year));
if (params?.month) searchParams.set('month', String(params.month));
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
if (params?.employee_id) searchParams.set('employee_id', String(params.employee_id));
if (params?.start_date) searchParams.set('start_date', params.start_date);
if (params?.end_date) searchParams.set('end_date', params.end_date);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
if (params?.search) searchParams.set('search', params.search);
if (params?.year) searchParams.set('year', String(params.year));
if (params?.month) searchParams.set('month', String(params.month));
if (params?.status && params.status !== 'all') searchParams.set('status', params.status);
if (params?.employee_id) searchParams.set('employee_id', String(params.employee_id));
if (params?.start_date) searchParams.set('start_date', params.start_date);
if (params?.end_date) searchParams.set('end_date', params.end_date);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.per_page) searchParams.set('per_page', String(params.per_page));
const url = `${API_URL}/v1/salaries${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const url = `${API_URL}/v1/salaries${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const result: SalaryListResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '급여 목록을 불러오는데 실패했습니다.' };
}
return {
success: true,
data: result.data.data.map(transformApiToFrontend),
pagination: {
total: result.data.total,
currentPage: result.data.current_page,
lastPage: result.data.last_page,
},
};
} catch (error) {
console.error('[getSalaries] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '급여 목록을 불러오는데 실패했습니다.' };
}
const result: SalaryListResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '급여 목록을 불러오는데 실패했습니다.' };
}
return {
success: true,
data: result.data.data.map(transformApiToFrontend),
pagination: {
total: result.data.total,
currentPage: result.data.current_page,
lastPage: result.data.last_page,
},
};
}
/**
@@ -234,28 +215,22 @@ export async function getSalary(id: string): Promise<{
data?: SalaryDetail;
error?: string
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/salaries/${id}`, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/${id}`, { method: 'GET' });
const result: SalaryResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '급여 정보를 불러오는데 실패했습니다.' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
} catch (error) {
console.error('[getSalary] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '급여 정보를 불러오는데 실패했습니다.' };
}
const result: SalaryResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '급여 정보를 불러오는데 실패했습니다.' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
}
/**
@@ -265,28 +240,25 @@ export async function updateSalaryStatus(
id: string,
status: PaymentStatus
): Promise<{ success: boolean; data?: SalaryRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/salaries/${id}/status`, {
method: 'PATCH',
headers,
body: JSON.stringify({ status }),
});
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/${id}/status`, {
method: 'PATCH',
body: JSON.stringify({ status }),
});
const result: SalaryResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
} catch (error) {
console.error('[updateSalaryStatus] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '상태 변경에 실패했습니다.' };
}
const result: SalaryResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
}
return {
success: true,
data: transformApiToFrontend(result.data),
};
}
/**
@@ -296,31 +268,28 @@ export async function bulkUpdateSalaryStatus(
ids: string[],
status: PaymentStatus
): Promise<{ success: boolean; updatedCount?: number; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/salaries/bulk-update-status`, {
method: 'POST',
headers,
body: JSON.stringify({
ids: ids.map(id => parseInt(id, 10)),
status
}),
});
const { response, error } = await serverFetch(`${API_URL}/v1/salaries/bulk-update-status`, {
method: 'POST',
body: JSON.stringify({
ids: ids.map(id => parseInt(id, 10)),
status
}),
});
const result: BulkUpdateResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '일괄 상태 변경에 실패했습니다.' };
}
return {
success: true,
updatedCount: result.data.updated_count,
};
} catch (error) {
console.error('[bulkUpdateSalaryStatus] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '일괄 상태 변경에 실패했습니다.' };
}
const result: BulkUpdateResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '일괄 상태 변경에 실패했습니다.' };
}
return {
success: true,
updatedCount: result.data.updated_count,
};
}
/**
@@ -346,44 +315,38 @@ export async function getSalaryStatistics(params?: {
};
error?: string
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams();
if (params?.year) searchParams.set('year', String(params.year));
if (params?.month) searchParams.set('month', String(params.month));
if (params?.start_date) searchParams.set('start_date', params.start_date);
if (params?.end_date) searchParams.set('end_date', params.end_date);
if (params?.year) searchParams.set('year', String(params.year));
if (params?.month) searchParams.set('month', String(params.month));
if (params?.start_date) searchParams.set('start_date', params.start_date);
if (params?.end_date) searchParams.set('end_date', params.end_date);
const url = `${API_URL}/v1/salaries/statistics${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const url = `${API_URL}/v1/salaries/statistics${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;
const { response, error } = await serverFetch(url, { method: 'GET' });
const result: StatisticsResponse = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '통계 정보를 불러오는데 실패했습니다.' };
}
return {
success: true,
data: {
totalNetPayment: result.data.total_net_payment,
totalBaseSalary: result.data.total_base_salary,
totalAllowance: result.data.total_allowance,
totalOvertime: result.data.total_overtime,
totalBonus: result.data.total_bonus,
totalDeduction: result.data.total_deduction,
count: result.data.count,
scheduledCount: result.data.scheduled_count,
completedCount: result.data.completed_count,
},
};
} catch (error) {
console.error('[getSalaryStatistics] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
if (error || !response) {
return { success: false, error: error?.message || '통계 정보를 불러오는데 실패했습니다.' };
}
const result: StatisticsResponse = await response.json();
if (!result.success) {
return { success: false, error: result.message || '통계 정보를 불러오는데 실패했습니다.' };
}
return {
success: true,
data: {
totalNetPayment: result.data.total_net_payment,
totalBaseSalary: result.data.total_base_salary,
totalAllowance: result.data.total_allowance,
totalOvertime: result.data.total_overtime,
totalBonus: result.data.total_bonus,
totalDeduction: result.data.total_deduction,
count: result.data.count,
scheduledCount: result.data.scheduled_count,
completedCount: result.data.completed_count,
},
};
}

View File

@@ -21,7 +21,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
// ============================================
// 타입 정의
@@ -171,25 +171,6 @@ interface PaginatedResponse<T> {
last_page: number;
}
// ============================================
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// API URL
const API_URL = `${process.env.NEXT_PUBLIC_API_URL || process.env.API_URL}/api`;
@@ -266,7 +247,6 @@ export async function getLeaves(params?: GetLeavesParams): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params) {
@@ -286,10 +266,11 @@ export async function getLeaves(params?: GetLeavesParams): Promise<{
const queryString = searchParams.toString();
const url = `${API_URL}/v1/leaves${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || '휴가 목록 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
@@ -328,11 +309,11 @@ export async function getLeaveById(id: number): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/leaves/${id}`, {
method: 'GET',
headers,
});
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}`, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || '휴가 조회에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
@@ -364,10 +345,8 @@ export async function createLeave(
data: CreateLeaveRequest
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/leaves`, {
const { response, error } = await serverFetch(`${API_URL}/v1/leaves`, {
method: 'POST',
headers,
body: JSON.stringify({
user_id: data.userId,
leave_type: data.leaveType,
@@ -378,6 +357,10 @@ export async function createLeave(
}),
});
if (error || !response) {
return { success: false, error: error?.message || '휴가 신청에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
if (result.success && result.data) {
@@ -409,13 +392,15 @@ export async function approveLeave(
comment?: string
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/leaves/${id}/approve`, {
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}/approve`, {
method: 'POST',
headers,
body: JSON.stringify({ comment }),
});
if (error || !response) {
return { success: false, error: error?.message || '휴가 승인에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
if (result.success && result.data) {
@@ -447,13 +432,15 @@ export async function rejectLeave(
reason: string
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/leaves/${id}/reject`, {
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}/reject`, {
method: 'POST',
headers,
body: JSON.stringify({ reason }),
});
if (error || !response) {
return { success: false, error: error?.message || '휴가 반려에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
if (result.success && result.data) {
@@ -485,13 +472,15 @@ export async function cancelLeave(
reason?: string
): Promise<{ success: boolean; data?: LeaveRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/leaves/${id}/cancel`, {
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}/cancel`, {
method: 'POST',
headers,
body: JSON.stringify({ reason }),
});
if (error || !response) {
return { success: false, error: error?.message || '휴가 취소에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
if (result.success && result.data) {
@@ -524,15 +513,15 @@ export async function getMyLeaveBalance(year?: number): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = year
? `${API_URL}/v1/leaves/balance?year=${year}`
: `${API_URL}/v1/leaves/balance`;
const response = await fetch(url, {
method: 'GET',
headers,
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || '잔여 휴가 조회에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
@@ -569,15 +558,15 @@ export async function getUserLeaveBalance(
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = year
? `${API_URL}/v1/leaves/balance/${userId}?year=${year}`
: `${API_URL}/v1/leaves/balance/${userId}`;
const response = await fetch(url, {
method: 'GET',
headers,
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || '잔여 휴가 조회에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
@@ -609,10 +598,8 @@ export async function setLeaveBalance(
data: SetLeaveBalanceRequest
): Promise<{ success: boolean; data?: LeaveBalance; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/leaves/balance`, {
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/balance`, {
method: 'PUT',
headers,
body: JSON.stringify({
user_id: data.userId,
year: data.year,
@@ -620,6 +607,10 @@ export async function setLeaveBalance(
}),
});
if (error || !response) {
return { success: false, error: error?.message || '잔여 휴가 설정에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
if (result.success && result.data) {
@@ -648,12 +639,14 @@ export async function setLeaveBalance(
*/
export async function deleteLeave(id: number): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/leaves/${id}`, {
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/${id}`, {
method: 'DELETE',
headers,
});
if (error || !response) {
return { success: false, error: error?.message || '휴가 삭제에 실패했습니다.' };
}
const result: ApiResponse<null> = await response.json();
if (result.success) {
@@ -748,7 +741,6 @@ export async function getLeaveBalances(params?: GetLeaveBalancesParams): Promise
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params) {
@@ -764,10 +756,11 @@ export async function getLeaveBalances(params?: GetLeaveBalancesParams): Promise
const queryString = searchParams.toString();
const url = `${API_URL}/v1/leaves/balances${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || '휴가 사용현황 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
@@ -890,7 +883,6 @@ export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params) {
@@ -910,10 +902,11 @@ export async function getLeaveGrants(params?: GetLeaveGrantsParams): Promise<{
const queryString = searchParams.toString();
const url = `${API_URL}/v1/leaves/grants${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
method: 'GET',
headers,
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || '휴가 부여 이력 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<Record<string, unknown>>> = await response.json();
@@ -950,10 +943,8 @@ export async function createLeaveGrant(
data: CreateLeaveGrantRequest
): Promise<{ success: boolean; data?: LeaveGrantRecord; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/leaves/grants`, {
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/grants`, {
method: 'POST',
headers,
body: JSON.stringify({
user_id: data.userId,
grant_type: data.grantType,
@@ -963,6 +954,10 @@ export async function createLeaveGrant(
}),
});
if (error || !response) {
return { success: false, error: error?.message || '휴가 부여에 실패했습니다.' };
}
const result: ApiResponse<Record<string, unknown>> = await response.json();
if (result.success && result.data) {
@@ -991,12 +986,14 @@ export async function createLeaveGrant(
*/
export async function deleteLeaveGrant(id: number): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${API_URL}/v1/leaves/grants/${id}`, {
const { response, error } = await serverFetch(`${API_URL}/v1/leaves/grants/${id}`, {
method: 'DELETE',
headers,
});
if (error || !response) {
return { success: false, error: error?.message || '휴가 부여 삭제에 실패했습니다.' };
}
const result: ApiResponse<null> = await response.json();
if (result.success) {
@@ -1069,14 +1066,13 @@ export async function getActiveEmployees(): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${API_URL}/v1/employees?status=active&per_page=100`;
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || '직원 목록 조회에 실패했습니다.' };
}
const result = await response.json();

View File

@@ -29,6 +29,7 @@ import {
import { Search, Plus, Edit, Trash2, Package, Loader2 } from 'lucide-react';
import { TableLoadingSpinner } from '@/components/ui/loading-spinner';
import { useItemList } from '@/hooks/useItemList';
import { handleApiError } from '@/lib/api/error-handler';
import {
IntegratedListTemplateV2,
type TabOption,
@@ -183,10 +184,15 @@ export default function ItemListClient() {
},
});
// 🚨 401 인증 에러 시 자동 로그인 페이지 리다이렉트
if (!response.ok) {
await handleApiError(response);
}
const result = await response.json();
console.log('[Delete] 응답:', { status: response.status, result });
if (response.ok && result.success) {
if (result.success) {
refresh();
} else {
throw new Error(result.message || '삭제에 실패했습니다.');
@@ -239,6 +245,13 @@ export default function ItemListClient() {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});
// 🚨 401 인증 에러 시 자동 로그인 페이지 리다이렉트
if (response.status === 401) {
await handleApiError(response);
return; // 리다이렉트 후 중단
}
const result = await response.json();
if (response.ok && result.success) {
successCount++;

View File

@@ -13,7 +13,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
ReceivingItem,
ReceivingDetail,
@@ -146,19 +146,6 @@ function transformProcessDataToApi(
};
}
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
currentPage: number;
@@ -180,9 +167,9 @@ export async function getReceivings(params?: {
data: ReceivingItem[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -197,27 +184,33 @@ export async function getReceivings(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings${queryString ? `?${queryString}` : ''}`;
console.log('[ReceivingActions] GET receivings:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.warn('[ReceivingActions] GET receivings error:', response.status);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '입고 목록 조회에 실패했습니다.',
};
}
const result = await response.json();
if (!result.success) {
if (!response.ok || !result.success) {
return {
success: false,
data: [],
@@ -262,46 +255,32 @@ export async function getReceivingStats(): Promise<{
success: boolean;
data?: ReceivingStats;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/stats`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET', cache: 'no-store' }
);
if (!response.ok) {
console.warn('[ReceivingActions] GET stats error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '입고 통계 조회에 실패했습니다.' };
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '입고 통계 조회에 실패했습니다.',
};
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '입고 통계 조회에 실패했습니다.' };
}
return {
success: true,
data: transformApiToStats(result.data),
};
return { success: true, data: transformApiToStats(result.data) };
} catch (error) {
console.error('[ReceivingActions] getReceivingStats error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -310,88 +289,65 @@ export async function getReceivingById(id: string): Promise<{
success: boolean;
data?: ReceivingDetail;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET', cache: 'no-store' }
);
if (!response.ok) {
console.error('[ReceivingActions] GET receiving error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '입고 조회에 실패했습니다.' };
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '입고 조회에 실패했습니다.',
};
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '입고 조회에 실패했습니다.' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
console.error('[ReceivingActions] getReceivingById error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 입고 등록 =====
export async function createReceiving(
data: Partial<ReceivingDetail>
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string }> {
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[ReceivingActions] POST receiving request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
{ method: 'POST', body: JSON.stringify(apiData) }
);
const result = await response.json();
console.log('[ReceivingActions] POST receiving response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '입고 등록에 실패했습니다.',
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
if (!response) {
return { success: false, error: '입고 등록에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고 등록에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
console.error('[ReceivingActions] createReceiving error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -399,77 +355,64 @@ export async function createReceiving(
export async function updateReceiving(
id: string,
data: Partial<ReceivingDetail>
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string }> {
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[ReceivingActions] PUT receiving request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
{ method: 'PUT', body: JSON.stringify(apiData) }
);
const result = await response.json();
console.log('[ReceivingActions] PUT receiving response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '입고 수정에 실패했습니다.',
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
if (!response) {
return { success: false, error: '입고 수정에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고 수정에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
console.error('[ReceivingActions] updateReceiving error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 입고 삭제 =====
export async function deleteReceiving(
id: string
): Promise<{ success: boolean; error?: string }> {
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}`,
{
method: 'DELETE',
headers,
}
{ method: 'DELETE' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '입고 삭제에 실패했습니다.' };
}
const result = await response.json();
console.log('[ReceivingActions] DELETE receiving response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '입고 삭제에 실패했습니다.',
};
return { success: false, error: result.message || '입고 삭제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
console.error('[ReceivingActions] deleteReceiving error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -477,41 +420,32 @@ export async function deleteReceiving(
export async function processReceiving(
id: string,
data: ReceivingProcessFormData
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string }> {
): Promise<{ success: boolean; data?: ReceivingDetail; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformProcessDataToApi(data);
console.log('[ReceivingActions] POST process request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/receivings/${id}/process`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
{ method: 'POST', body: JSON.stringify(apiData) }
);
const result = await response.json();
console.log('[ReceivingActions] POST process response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '입고처리에 실패했습니다.',
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
if (!response) {
return { success: false, error: '입고처리에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '입고처리에 실패했습니다.' };
}
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
console.error('[ReceivingActions] processReceiving error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
}

View File

@@ -10,7 +10,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
StockItem,
StockDetail,
@@ -156,18 +156,6 @@ function transformApiToStats(data: StockApiStatsResponse): StockStats {
};
}
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
@@ -192,9 +180,9 @@ export async function getStocks(params?: {
data: StockItem[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -213,27 +201,33 @@ export async function getStocks(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks${queryString ? `?${queryString}` : ''}`;
console.log('[StockActions] GET stocks:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.warn('[StockActions] GET stocks error:', response.status);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '재고 목록 조회에 실패했습니다.',
};
}
const result = await response.json();
if (!result.success) {
if (!response.ok || !result.success) {
return {
success: false,
data: [],
@@ -278,46 +272,32 @@ export async function getStockStats(): Promise<{
success: boolean;
data?: StockStats;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/stats`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET', cache: 'no-store' }
);
if (!response.ok) {
console.warn('[StockActions] GET stats error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '재고 통계 조회에 실패했습니다.' };
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '재고 통계 조회에 실패했습니다.',
};
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '재고 통계 조회에 실패했습니다.' };
}
return {
success: true,
data: transformApiToStats(result.data),
};
return { success: true, data: transformApiToStats(result.data) };
} catch (error) {
console.error('[StockActions] getStockStats error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -326,46 +306,32 @@ export async function getStockStatsByType(): Promise<{
success: boolean;
data?: StockApiStatsByTypeResponse;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/stats-by-type`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET', cache: 'no-store' }
);
if (!response.ok) {
console.warn('[StockActions] GET stats-by-type error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '품목유형별 통계 조회에 실패했습니다.' };
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '품목유형별 통계 조회에 실패했습니다.',
};
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '품목유형별 통계 조회에 실패했습니다.' };
}
return {
success: true,
data: result.data,
};
return { success: true, data: result.data };
} catch (error) {
console.error('[StockActions] getStockStatsByType error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -374,45 +340,31 @@ export async function getStockById(id: string): Promise<{
success: boolean;
data?: StockDetail;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/stocks/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
{ method: 'GET', cache: 'no-store' }
);
if (!response.ok) {
console.error('[StockActions] GET stock error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '재고 조회에 실패했습니다.' };
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '재고 조회에 실패했습니다.',
};
if (!response.ok || !result.success || !result.data) {
return { success: false, error: result.message || '재고 조회에 실패했습니다.' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
console.error('[StockActions] getStockById error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}

View File

@@ -163,7 +163,8 @@ export function OrderDocumentModal({
{/* 버튼 영역 - 고정 */}
<div className="flex-shrink-0 flex items-center gap-2 px-6 py-3 bg-muted/30 border-b">
<Button variant="outline" size="sm" onClick={handleSharePdf}>
{/* TODO: 공유 기능 추가 예정 - PDF 다운로드, 이메일, 팩스, 카카오톡 공유 */}
{/* <Button variant="outline" size="sm" onClick={handleSharePdf}>
<FileDown className="h-4 w-4 mr-1" />
PDF
</Button>
@@ -178,7 +179,7 @@ export function OrderDocumentModal({
<Button variant="outline" size="sm" onClick={handleShareKakao}>
<Share2 className="h-4 w-4 mr-1" />
공유
</Button>
</Button> */}
<Button variant="outline" size="sm" onClick={handlePrint}>
<Printer className="h-4 w-4 mr-1" />

View File

@@ -17,7 +17,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
ShipmentItem,
ShipmentDetail,
@@ -233,18 +233,6 @@ function transformEditFormToApi(
return result;
}
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
@@ -273,9 +261,9 @@ export async function getShipments(params?: {
data: ShipmentItem[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -304,19 +292,28 @@ export async function getShipments(params?: {
console.log('[ShipmentActions] GET shipments:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.warn('[ShipmentActions] GET shipments error:', response.status);
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
console.warn('[ShipmentActions] GET shipments error:', response?.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response?.status}`,
};
}
@@ -367,46 +364,36 @@ export async function getShipmentStats(): Promise<{
success: boolean;
data?: ShipmentStats;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/stats`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.warn('[ShipmentActions] GET stats error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response || !response.ok) {
console.warn('[ShipmentActions] GET stats error:', response?.status);
return { success: false, error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '출하 통계 조회에 실패했습니다.',
};
return { success: false, error: result.message || '출하 통계 조회에 실패했습니다.' };
}
return {
success: true,
data: transformApiToStats(result.data),
};
return { success: true, data: transformApiToStats(result.data) };
} catch (error) {
console.error('[ShipmentActions] getShipmentStats error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -415,46 +402,36 @@ export async function getShipmentStatsByStatus(): Promise<{
success: boolean;
data?: ShipmentApiStatsByStatusResponse;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/stats-by-status`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.warn('[ShipmentActions] GET stats-by-status error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response || !response.ok) {
console.warn('[ShipmentActions] GET stats-by-status error:', response?.status);
return { success: false, error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '상태별 통계 조회에 실패했습니다.',
};
return { success: false, error: result.message || '상태별 통계 조회에 실패했습니다.' };
}
return {
success: true,
data: result.data,
};
return { success: true, data: result.data };
} catch (error) {
console.error('[ShipmentActions] getShipmentStatsByStatus error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -463,88 +440,74 @@ export async function getShipmentById(id: string): Promise<{
success: boolean;
data?: ShipmentDetail;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.error('[ShipmentActions] GET shipment error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response || !response.ok) {
console.error('[ShipmentActions] GET shipment error:', response?.status);
return { success: false, error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success || !result.data) {
return {
success: false,
error: result.message || '출하 조회에 실패했습니다.',
};
return { success: false, error: result.message || '출하 조회에 실패했습니다.' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
console.error('[ShipmentActions] getShipmentById error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 출하 등록 =====
export async function createShipment(
data: ShipmentCreateFormData
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string }> {
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformCreateFormToApi(data);
console.log('[ShipmentActions] POST shipment request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '출하 등록에 실패했습니다.' };
}
const result = await response.json();
console.log('[ShipmentActions] POST shipment response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '출하 등록에 실패했습니다.',
};
return { success: false, error: result.message || '출하 등록에 실패했습니다.' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
console.error('[ShipmentActions] createShipment error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -552,42 +515,38 @@ export async function createShipment(
export async function updateShipment(
id: string,
data: Partial<ShipmentEditFormData>
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string }> {
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformEditFormToApi(data);
console.log('[ShipmentActions] PUT shipment request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '출하 수정에 실패했습니다.' };
}
const result = await response.json();
console.log('[ShipmentActions] PUT shipment response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '출하 수정에 실패했습니다.',
};
return { success: false, error: result.message || '출하 수정에 실패했습니다.' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
console.error('[ShipmentActions] updateShipment error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -603,10 +562,8 @@ export async function updateShipmentStatus(
driverContact?: string;
confirmedArrival?: string;
}
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string }> {
): Promise<{ success: boolean; data?: ShipmentDetail; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData: Record<string, unknown> = { status };
if (additionalData?.loadingTime) apiData.loading_time = additionalData.loadingTime;
if (additionalData?.loadingCompletedAt) apiData.loading_completed_at = additionalData.loadingCompletedAt;
@@ -617,70 +574,67 @@ export async function updateShipmentStatus(
console.log('[ShipmentActions] PATCH status request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}/status`,
{
method: 'PATCH',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '상태 변경에 실패했습니다.' };
}
const result = await response.json();
console.log('[ShipmentActions] PATCH status response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '상태 변경에 실패했습니다.',
};
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
}
return {
success: true,
data: transformApiToDetail(result.data),
};
return { success: true, data: transformApiToDetail(result.data) };
} catch (error) {
console.error('[ShipmentActions] updateShipmentStatus error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 출하 삭제 =====
export async function deleteShipment(
id: string
): Promise<{ success: boolean; error?: string }> {
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/${id}`,
{
method: 'DELETE',
headers,
}
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '출하 삭제에 실패했습니다.' };
}
const result = await response.json();
console.log('[ShipmentActions] DELETE shipment response:', result);
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '출하 삭제에 실패했습니다.',
};
return { success: false, error: result.message || '출하 삭제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
console.error('[ShipmentActions] deleteShipment error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -689,49 +643,36 @@ export async function getLotOptions(): Promise<{
success: boolean;
data: LotOption[];
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/options/lots`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.warn('[ShipmentActions] GET lot options error:', response.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, data: [], error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response || !response.ok) {
console.warn('[ShipmentActions] GET lot options error:', response?.status);
return { success: false, data: [], error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
error: result.message || 'LOT 옵션 조회에 실패했습니다.',
};
return { success: false, data: [], error: result.message || 'LOT 옵션 조회에 실패했습니다.' };
}
return {
success: true,
data: result.data || [],
};
return { success: true, data: result.data || [] };
} catch (error) {
console.error('[ShipmentActions] getLotOptions error:', error);
return {
success: false,
data: [],
error: '서버 오류가 발생했습니다.',
};
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}
@@ -740,49 +681,36 @@ export async function getLogisticsOptions(): Promise<{
success: boolean;
data: LogisticsOption[];
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/options/logistics`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.warn('[ShipmentActions] GET logistics options error:', response.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, data: [], error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response || !response.ok) {
console.warn('[ShipmentActions] GET logistics options error:', response?.status);
return { success: false, data: [], error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
error: result.message || '물류사 옵션 조회에 실패했습니다.',
};
return { success: false, data: [], error: result.message || '물류사 옵션 조회에 실패했습니다.' };
}
return {
success: true,
data: result.data || [],
};
return { success: true, data: result.data || [] };
} catch (error) {
console.error('[ShipmentActions] getLogisticsOptions error:', error);
return {
success: false,
data: [],
error: '서버 오류가 발생했습니다.',
};
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}
@@ -791,48 +719,35 @@ export async function getVehicleTonnageOptions(): Promise<{
success: boolean;
data: VehicleTonnageOption[];
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/shipments/options/vehicle-tonnage`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.warn('[ShipmentActions] GET vehicle tonnage options error:', response.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, data: [], error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response || !response.ok) {
console.warn('[ShipmentActions] GET vehicle tonnage options error:', response?.status);
return { success: false, data: [], error: `API 오류: ${response?.status}` };
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
error: result.message || '차량 톤수 옵션 조회에 실패했습니다.',
};
return { success: false, data: [], error: result.message || '차량 톤수 옵션 조회에 실패했습니다.' };
}
return {
success: true,
data: result.data || [],
};
return { success: true, data: result.data || [] };
} catch (error) {
console.error('[ShipmentActions] getVehicleTonnageOptions error:', error);
return {
success: false,
data: [],
error: '서버 오류가 발생했습니다.',
};
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}

View File

@@ -13,7 +13,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { PricingData, ItemInfo } from './types';
// API 응답 타입
@@ -83,21 +83,6 @@ interface PriceApiData {
}>;
}
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* API 데이터 → 프론트엔드 타입 변환
*/
@@ -178,17 +163,24 @@ function transformFrontendToApi(data: PricingData): Record<string, unknown> {
*/
export async function getPricingById(id: string): Promise<PricingData | null> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error) {
console.error('[PricingActions] GET pricing error:', error.message);
return null;
}
if (!response) {
console.error('[PricingActions] GET pricing: 응답이 없습니다.');
return null;
}
if (!response.ok) {
console.error('[PricingActions] GET pricing error:', response.status);
return null;
@@ -215,17 +207,24 @@ export async function getPricingById(id: string): Promise<PricingData | null> {
*/
export async function getItemInfo(itemId: string): Promise<ItemInfo | null> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/items/${itemId}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error) {
console.error('[PricingActions] getItemInfo error:', error.message);
return null;
}
if (!response) {
console.error('[PricingActions] getItemInfo: 응답이 없습니다.');
return null;
}
if (!response.ok) {
console.error('[PricingActions] Item not found:', itemId);
return null;
@@ -257,22 +256,35 @@ export async function getItemInfo(itemId: string): Promise<ItemInfo | null> {
*/
export async function createPricing(
data: PricingData
): Promise<{ success: boolean; data?: PricingData; error?: string }> {
): Promise<{ success: boolean; data?: PricingData; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[PricingActions] POST pricing request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '단가 등록에 실패했습니다.',
};
}
const result = await response.json();
console.log('[PricingActions] POST pricing response:', result);
@@ -303,9 +315,8 @@ export async function updatePricing(
id: string,
data: PricingData,
changeReason?: string
): Promise<{ success: boolean; data?: PricingData; error?: string }> {
): Promise<{ success: boolean; data?: PricingData; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = {
...transformFrontendToApi(data),
change_reason: changeReason || null,
@@ -313,15 +324,29 @@ export async function updatePricing(
console.log('[PricingActions] PUT pricing request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '단가 수정에 실패했습니다.',
};
}
const result = await response.json();
console.log('[PricingActions] PUT pricing response:', result);
@@ -348,18 +373,30 @@ export async function updatePricing(
/**
* 단가 삭제
*/
export async function deletePricing(id: string): Promise<{ success: boolean; error?: string }> {
export async function deletePricing(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}`,
{
method: 'DELETE',
headers,
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '단가 삭제에 실패했습니다.',
};
}
const result = await response.json();
console.log('[PricingActions] DELETE pricing response:', result);
@@ -383,18 +420,30 @@ export async function deletePricing(id: string): Promise<{ success: boolean; err
/**
* 단가 확정
*/
export async function finalizePricing(id: string): Promise<{ success: boolean; data?: PricingData; error?: string }> {
export async function finalizePricing(id: string): Promise<{ success: boolean; data?: PricingData; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${id}/finalize`,
{
method: 'POST',
headers,
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '단가 확정에 실패했습니다.',
};
}
const result = await response.json();
console.log('[PricingActions] POST finalize response:', result);
@@ -432,19 +481,32 @@ export async function getPricingRevisions(priceId: string): Promise<{
afterSnapshot: Record<string, unknown>;
}>;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/pricing/${priceId}/revisions`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '이력 조회에 실패했습니다.',
};
}
const result = await response.json();
console.log('[PricingActions] GET revisions response:', result);

View File

@@ -1,24 +1,8 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { Process, ProcessFormData, ClassificationRule } from '@/types/process';
// ============================================================================
// API 헤더 생성
// ============================================================================
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ============================================================================
// API 타입 정의
// ============================================================================
@@ -144,9 +128,8 @@ export async function getProcessList(params?: {
q?: string;
status?: string;
process_type?: string;
}): Promise<{ success: boolean; data?: { items: Process[]; total: number; page: number; totalPages: number }; error?: string }> {
}): Promise<{ success: boolean; data?: { items: Process[]; total: number; page: number; totalPages: number }; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -155,11 +138,19 @@ export async function getProcessList(params?: {
if (params?.status) searchParams.set('status', params.status);
if (params?.process_type) searchParams.set('process_type', params.process_type);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.API_URL}/v1/processes?${searchParams.toString()}`,
{ method: 'GET', headers, cache: 'no-store' }
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '목록 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<ApiProcess>> = await response.json();
if (!response.ok || !result.success) {
@@ -184,16 +175,21 @@ export async function getProcessList(params?: {
/**
* 공정 상세 조회
*/
export async function getProcessById(id: string): Promise<{ success: boolean; data?: Process; error?: string }> {
export async function getProcessById(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '조회에 실패했습니다.' };
}
const result: ApiResponse<ApiProcess> = await response.json();
if (!response.ok || !result.success) {
@@ -210,17 +206,23 @@ export async function getProcessById(id: string): Promise<{ success: boolean; da
/**
* 공정 생성
*/
export async function createProcess(data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string }> {
export async function createProcess(data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const response = await fetch(`${process.env.API_URL}/v1/processes`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes`, {
method: 'POST',
headers,
body: JSON.stringify(apiData),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '등록에 실패했습니다.' };
}
const result: ApiResponse<ApiProcess> = await response.json();
if (!response.ok || !result.success) {
@@ -237,17 +239,23 @@ export async function createProcess(data: ProcessFormData): Promise<{ success: b
/**
* 공정 수정
*/
export async function updateProcess(id: string, data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string }> {
export async function updateProcess(id: string, data: ProcessFormData): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}`, {
method: 'PUT',
headers,
body: JSON.stringify(apiData),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '수정에 실패했습니다.' };
}
const result: ApiResponse<ApiProcess> = await response.json();
if (!response.ok || !result.success) {
@@ -264,15 +272,20 @@ export async function updateProcess(id: string, data: ProcessFormData): Promise<
/**
* 공정 삭제
*/
export async function deleteProcess(id: string): Promise<{ success: boolean; error?: string }> {
export async function deleteProcess(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}`, {
method: 'DELETE',
headers,
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '삭제에 실패했습니다.' };
}
const result: ApiResponse<null> = await response.json();
if (!response.ok || !result.success) {
@@ -289,16 +302,21 @@ export async function deleteProcess(id: string): Promise<{ success: boolean; err
/**
* 공정 일괄 삭제
*/
export async function deleteProcesses(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string }> {
export async function deleteProcesses(ids: string[]): Promise<{ success: boolean; deletedCount?: number; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes`, {
method: 'DELETE',
headers,
body: JSON.stringify({ ids: ids.map((id) => parseInt(id, 10)) }),
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '일괄 삭제에 실패했습니다.' };
}
const result: ApiResponse<{ deleted_count: number }> = await response.json();
if (!response.ok || !result.success) {
@@ -315,15 +333,20 @@ export async function deleteProcesses(ids: string[]): Promise<{ success: boolean
/**
* 공정 상태 토글
*/
export async function toggleProcessActive(id: string): Promise<{ success: boolean; data?: Process; error?: string }> {
export async function toggleProcessActive(id: string): Promise<{ success: boolean; data?: Process; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/${id}/toggle`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/${id}/toggle`, {
method: 'PATCH',
headers,
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '상태 변경에 실패했습니다.' };
}
const result: ApiResponse<ApiProcess> = await response.json();
if (!response.ok || !result.success) {
@@ -344,16 +367,22 @@ export async function getProcessOptions(): Promise<{
success: boolean;
data?: Array<{ id: string; processCode: string; processName: string; processType: string; department: string }>;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/options`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/options`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '옵션 조회에 실패했습니다.' };
}
const result: ApiResponse<Array<{ id: number; process_code: string; process_name: string; process_type: string; department: string }>> =
await response.json();
@@ -384,16 +413,22 @@ export async function getProcessStats(): Promise<{
success: boolean;
data?: { total: number; active: number; inactive: number; byType: Record<string, number> };
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(`${process.env.API_URL}/v1/processes/stats`, {
const { response, error } = await serverFetch(`${process.env.API_URL}/v1/processes/stats`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '통계 조회에 실패했습니다.' };
}
const result: ApiResponse<{ total: number; active: number; inactive: number; by_type: Record<string, number> }> = await response.json();
if (!response.ok || !result.success) {

View File

@@ -1,26 +1,14 @@
/**
* 생산 현황판 서버 액션
* API 연동 완료 (2025-12-26)
* serverFetch 마이그레이션 (2025-12-30)
*/
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { WorkOrder, WorkerStatus, ProcessType, DashboardStats } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 타입 =====
interface WorkOrderApiItem {
id: number;
@@ -101,9 +89,14 @@ export async function getDashboardData(processType?: ProcessType): Promise<{
stats: DashboardStats;
error?: string;
}> {
try {
const headers = await getApiHeaders();
const emptyResult = {
success: false,
workOrders: [] as WorkOrder[],
workerStatus: [] as WorkerStatus[],
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
};
try {
// 작업지시 목록 조회
const params = new URLSearchParams({ per_page: '100' });
if (processType && processType !== 'all') {
@@ -113,19 +106,21 @@ export async function getDashboardData(processType?: ProcessType): Promise<{
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders?${params.toString()}`;
console.log('[ProductionDashboardActions] GET:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
// serverFetch handles 401 with redirect, so we only check for other errors
if (error || !response) {
console.warn('[ProductionDashboardActions] GET error:', error?.message);
return {
...emptyResult,
error: error?.message || '데이터 조회에 실패했습니다.',
};
}
if (!response.ok) {
console.warn('[ProductionDashboardActions] GET error:', response.status);
return {
success: false,
workOrders: [],
workerStatus: [],
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
...emptyResult,
error: `API 오류: ${response.status}`,
};
}
@@ -134,10 +129,7 @@ export async function getDashboardData(processType?: ProcessType): Promise<{
if (!result.success) {
return {
success: false,
workOrders: [],
workerStatus: [],
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
...emptyResult,
error: result.message || '데이터 조회에 실패했습니다.',
};
}
@@ -192,10 +184,7 @@ export async function getDashboardData(processType?: ProcessType): Promise<{
} catch (error) {
console.error('[ProductionDashboardActions] getDashboardData error:', error);
return {
success: false,
workOrders: [],
workerStatus: [],
stats: { total: 0, waiting: 0, inProgress: 0, completed: 0, urgent: 0, delayed: 0 },
...emptyResult,
error: '서버 오류가 발생했습니다.',
};
}

View File

@@ -17,7 +17,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
WorkOrder,
WorkOrderStats,
@@ -32,19 +32,6 @@ import {
transformStatsApiToFrontend,
} from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
currentPage: number;
@@ -68,8 +55,13 @@ export async function getWorkOrders(params?: {
pagination: PaginationMeta;
error?: string;
}> {
const emptyResponse = {
success: false,
data: [] as WorkOrder[],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
};
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -89,29 +81,22 @@ export async function getWorkOrders(params?: {
console.log('[WorkOrderActions] GET work-orders:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { ...emptyResponse, error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.warn('[WorkOrderActions] GET work-orders error:', response.status);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: `API 오류: ${response.status}`,
};
return { ...emptyResponse, error: `API 오류: ${response.status}` };
}
const result = await response.json();
if (!result.success) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
...emptyResponse,
error: result.message || '작업지시 목록 조회에 실패했습니다.',
};
}
@@ -138,12 +123,7 @@ export async function getWorkOrders(params?: {
};
} catch (error) {
console.error('[WorkOrderActions] getWorkOrders error:', error);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '서버 오류가 발생했습니다.',
};
return { ...emptyResponse, error: '서버 오류가 발생했습니다.' };
}
}
@@ -154,23 +134,19 @@ export async function getWorkOrderStats(): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/stats`;
console.log('[WorkOrderActions] GET stats:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.warn('[WorkOrderActions] GET stats error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
return { success: false, error: `API 오류: ${response.status}` };
}
const result = await response.json();
@@ -190,10 +166,7 @@ export async function getWorkOrderStats(): Promise<{
};
} catch (error) {
console.error('[WorkOrderActions] getWorkOrderStats error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -204,23 +177,19 @@ export async function getWorkOrderById(id: string): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`;
console.log('[WorkOrderActions] GET work-order:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.error('[WorkOrderActions] GET work-order error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
return { success: false, error: `API 오류: ${response.status}` };
}
const result = await response.json();
@@ -238,10 +207,7 @@ export async function getWorkOrderById(id: string): Promise<{
};
} catch (error) {
console.error('[WorkOrderActions] getWorkOrderById error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -254,7 +220,6 @@ export async function createWorkOrder(
}
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = {
...transformFrontendToApi(data),
sales_order_id: data.salesOrderId,
@@ -264,15 +229,18 @@ export async function createWorkOrder(
console.log('[WorkOrderActions] POST work-order request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] POST work-order response:', result);
@@ -289,10 +257,7 @@ export async function createWorkOrder(
};
} catch (error) {
console.error('[WorkOrderActions] createWorkOrder error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -302,20 +267,22 @@ export async function updateWorkOrder(
data: Partial<WorkOrder>
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[WorkOrderActions] PUT work-order request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PUT work-order response:', result);
@@ -332,26 +299,22 @@ export async function updateWorkOrder(
};
} catch (error) {
console.error('[WorkOrderActions] updateWorkOrder error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
// ===== 작업지시 삭제 =====
export async function deleteWorkOrder(id: string): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}`,
{
method: 'DELETE',
headers,
}
{ method: 'DELETE' }
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] DELETE work-order response:', result);
@@ -365,10 +328,7 @@ export async function deleteWorkOrder(id: string): Promise<{ success: boolean; e
return { success: true };
} catch (error) {
console.error('[WorkOrderActions] deleteWorkOrder error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -378,19 +338,20 @@ export async function updateWorkOrderStatus(
status: WorkOrderStatus
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const headers = await getApiHeaders();
console.log('[WorkOrderActions] PATCH status request:', { status });
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`,
{
method: 'PATCH',
headers,
body: JSON.stringify({ status }),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PATCH status response:', result);
@@ -407,10 +368,7 @@ export async function updateWorkOrderStatus(
};
} catch (error) {
console.error('[WorkOrderActions] updateWorkOrderStatus error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -421,22 +379,23 @@ export async function assignWorkOrder(
teamId?: number
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const headers = await getApiHeaders();
const body: { assignee_id: number; team_id?: number } = { assignee_id: assigneeId };
if (teamId) body.team_id = teamId;
console.log('[WorkOrderActions] PATCH assign request:', body);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/assign`,
{
method: 'PATCH',
headers,
body: JSON.stringify(body),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PATCH assign response:', result);
@@ -453,10 +412,7 @@ export async function assignWorkOrder(
};
} catch (error) {
console.error('[WorkOrderActions] assignWorkOrder error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -466,19 +422,20 @@ export async function toggleBendingField(
field: string
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const headers = await getApiHeaders();
console.log('[WorkOrderActions] PATCH bending toggle request:', { field });
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/bending/toggle`,
{
method: 'PATCH',
headers,
body: JSON.stringify({ field }),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PATCH bending toggle response:', result);
@@ -495,10 +452,7 @@ export async function toggleBendingField(
};
} catch (error) {
console.error('[WorkOrderActions] toggleBendingField error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -512,19 +466,20 @@ export async function addWorkOrderIssue(
}
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const headers = await getApiHeaders();
console.log('[WorkOrderActions] POST issue request:', data);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/issues`,
{
method: 'POST',
headers,
body: JSON.stringify(data),
}
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] POST issue response:', result);
@@ -541,10 +496,7 @@ export async function addWorkOrderIssue(
};
} catch (error) {
console.error('[WorkOrderActions] addWorkOrderIssue error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -554,18 +506,17 @@ export async function resolveWorkOrderIssue(
issueId: string
): Promise<{ success: boolean; data?: WorkOrder; error?: string }> {
try {
const headers = await getApiHeaders();
console.log('[WorkOrderActions] PATCH issue resolve:', { workOrderId, issueId });
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues/${issueId}/resolve`,
{
method: 'PATCH',
headers,
}
{ method: 'PATCH' }
);
if (error || !response) {
return { success: false, error: error?.message || 'API 요청 실패' };
}
const result = await response.json();
console.log('[WorkOrderActions] PATCH issue resolve response:', result);
@@ -582,10 +533,7 @@ export async function resolveWorkOrderIssue(
};
} catch (error) {
console.error('[WorkOrderActions] resolveWorkOrderIssue error:', error);
return {
success: false,
error: '서버 오류가 발생했습니다.',
};
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
@@ -610,7 +558,6 @@ export async function getSalesOrdersForWorkOrder(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
// 작업지시 생성 가능한 상태만 조회 (예: 회계확인 완료)
@@ -623,19 +570,15 @@ export async function getSalesOrdersForWorkOrder(params?: {
console.log('[WorkOrderActions] GET sales-orders for work-order:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.warn('[WorkOrderActions] GET sales-orders error:', response.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
};
return { success: false, data: [], error: `API 오류: ${response.status}` };
}
const result = await response.json();
@@ -677,11 +620,7 @@ export async function getSalesOrdersForWorkOrder(params?: {
};
} catch (error) {
console.error('[WorkOrderActions] getSalesOrdersForWorkOrder error:', error);
return {
success: false,
data: [],
error: '서버 오류가 발생했습니다.',
};
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}
@@ -706,24 +645,19 @@ export async function getDepartmentsWithUsers(): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/departments/tree?with_users=1`;
console.log('[WorkOrderActions] GET departments with users:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
return { success: false, data: [], error: error?.message || 'API 요청 실패' };
}
if (!response.ok) {
console.warn('[WorkOrderActions] GET departments error:', response.status);
return {
success: false,
data: [],
error: `API 오류: ${response.status}`,
};
return { success: false, data: [], error: `API 오류: ${response.status}` };
}
const result = await response.json();
@@ -765,10 +699,6 @@ export async function getDepartmentsWithUsers(): Promise<{
};
} catch (error) {
console.error('[WorkOrderActions] getDepartmentsWithUsers error:', error);
return {
success: false,
data: [],
error: '서버 오류가 발생했습니다.',
};
return { success: false, data: [], error: '서버 오류가 발생했습니다.' };
}
}

View File

@@ -14,7 +14,7 @@
* - PATCH /api/v1/work-results/{id}/packaging - 포장 상태 토글
*/
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { ProcessType } from '../WorkOrders/types';
import type {
WorkResult,
@@ -28,19 +28,6 @@ import {
transformStatsApiToFrontend,
} from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 페이지네이션 타입 =====
interface PaginationMeta {
currentPage: number;
@@ -68,7 +55,6 @@ export async function getWorkResults(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -93,12 +79,20 @@ export async function getWorkResults(params?: {
console.log('[WorkResultActions] GET work-results:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error || !response) {
console.warn('[WorkResultActions] GET work-results error:', error?.message);
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: error?.message || '서버 오류가 발생했습니다.',
};
}
if (!response.ok) {
console.warn('[WorkResultActions] GET work-results error:', response.status);
return {
@@ -160,7 +154,6 @@ export async function getWorkResultStats(params?: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.workDateFrom) searchParams.set('work_date_from', params.workDateFrom);
@@ -174,12 +167,18 @@ export async function getWorkResultStats(params?: {
console.log('[WorkResultActions] GET stats:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error || !response) {
console.warn('[WorkResultActions] GET stats error:', error?.message);
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
if (!response.ok) {
console.warn('[WorkResultActions] GET stats error:', response.status);
return {
@@ -219,17 +218,22 @@ export async function getWorkResultById(id: string): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}`;
console.log('[WorkResultActions] GET work-result:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error || !response) {
console.error('[WorkResultActions] GET work-result error:', error?.message);
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
if (!response.ok) {
console.error('[WorkResultActions] GET work-result error:', response.status);
return {
@@ -281,8 +285,6 @@ export async function createWorkResult(data: {
error?: string;
}> {
try {
const headers = await getApiHeaders();
const apiData: Record<string, unknown> = {
work_order_id: data.workOrderId,
lot_no: data.lotNo,
@@ -302,15 +304,22 @@ export async function createWorkResult(data: {
console.log('[WorkResultActions] POST work-result request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
if (error || !response) {
console.error('[WorkResultActions] POST work-result error:', error?.message);
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkResultActions] POST work-result response:', result);
@@ -344,20 +353,26 @@ export async function updateWorkResult(
error?: string;
}> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[WorkResultActions] PUT work-result request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error || !response) {
console.error('[WorkResultActions] PUT work-result error:', error?.message);
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkResultActions] PUT work-result response:', result);
@@ -387,16 +402,21 @@ export async function deleteWorkResult(id: string): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}`,
{
method: 'DELETE',
headers,
}
);
if (error || !response) {
console.error('[WorkResultActions] DELETE work-result error:', error?.message);
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkResultActions] DELETE work-result response:', result);
@@ -424,18 +444,23 @@ export async function toggleInspection(id: string): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
console.log('[WorkResultActions] PATCH inspection toggle:', id);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}/inspection`,
{
method: 'PATCH',
headers,
}
);
if (error || !response) {
console.error('[WorkResultActions] PATCH inspection error:', error?.message);
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkResultActions] PATCH inspection response:', result);
@@ -466,18 +491,23 @@ export async function togglePackaging(id: string): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
console.log('[WorkResultActions] PATCH packaging toggle:', id);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-results/${id}/packaging`,
{
method: 'PATCH',
headers,
}
);
if (error || !response) {
console.error('[WorkResultActions] PATCH packaging error:', error?.message);
return {
success: false,
error: error?.message || '서버 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkResultActions] PATCH packaging response:', result);

View File

@@ -7,22 +7,9 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { WorkOrder, WorkOrderStatus } from '../ProductionDashboard/types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 타입 =====
interface WorkOrderApiItem {
id: number;
@@ -97,18 +84,21 @@ export async function getMyWorkOrders(): Promise<{
error?: string;
}> {
try {
const headers = await getApiHeaders();
// 작업 대기 + 작업중 상태만 조회 (완료 제외)
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders?per_page=100&assigned_to_me=1`;
console.log('[WorkerScreenActions] GET my work orders:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
console.warn('[WorkerScreenActions] GET error:', error?.message);
return {
success: false,
data: [],
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
if (!response.ok) {
console.warn('[WorkerScreenActions] GET error:', response.status);
@@ -156,14 +146,11 @@ export async function completeWorkOrder(
materials?: { materialId: number; quantity: number; lotNo?: string }[]
): Promise<{ success: boolean; lotNo?: string; error?: string }> {
try {
const headers = await getApiHeaders();
// 상태를 completed로 변경
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${id}/status`,
{
method: 'PATCH',
headers,
body: JSON.stringify({
status: 'completed',
materials,
@@ -171,6 +158,13 @@ export async function completeWorkOrder(
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkerScreenActions] Complete response:', result);
@@ -215,18 +209,21 @@ export async function getMaterialsForWorkOrder(
error?: string;
}> {
try {
const headers = await getApiHeaders();
// 작업지시 BOM 기준 자재 목록 조회
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/materials`;
console.log('[WorkerScreenActions] GET materials for work order:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
console.warn('[WorkerScreenActions] GET materials error:', error?.message);
return {
success: false,
data: [],
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
if (!response.ok) {
console.warn('[WorkerScreenActions] GET materials error:', response.status);
@@ -284,17 +281,21 @@ export async function registerMaterialInput(
materialIds: number[]
): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/material-inputs`,
{
method: 'POST',
headers,
body: JSON.stringify({ material_ids: materialIds }),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkerScreenActions] Register material input response:', result);
@@ -325,17 +326,21 @@ export async function reportIssue(
}
): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/issues`,
{
method: 'POST',
headers,
body: JSON.stringify(data),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkerScreenActions] Report issue response:', result);
@@ -385,17 +390,20 @@ export async function getProcessSteps(
error?: string;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/process-steps`;
console.log('[WorkerScreenActions] GET process steps:', url);
const response = await fetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
const { response, error } = await serverFetch(url, { method: 'GET' });
if (error || !response) {
console.warn('[WorkerScreenActions] GET process steps error:', error?.message);
return {
success: false,
data: [],
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
if (!response.ok) {
console.warn('[WorkerScreenActions] GET process steps error:', response.status);
@@ -471,17 +479,21 @@ export async function requestInspection(
stepId: string
): Promise<{ success: boolean; error?: string }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/work-orders/${workOrderId}/process-steps/${stepId}/inspection-request`,
{
method: 'POST',
headers,
body: JSON.stringify({}),
}
);
if (error || !response) {
return {
success: false,
error: error?.message || '네트워크 오류가 발생했습니다.',
};
}
const result = await response.json();
console.log('[WorkerScreenActions] Inspection request response:', result);

View File

@@ -3,6 +3,7 @@
/**
* 검사 관리 Server Actions
* API 연동 완료 (2025-12-26)
* fetch-wrapper 마이그레이션 완료 (2025-12-30)
*
* API Endpoints:
* - GET /api/v1/inspections - 목록 조회
@@ -14,7 +15,7 @@
* - PATCH /api/v1/inspections/{id}/complete - 검사 완료 처리
*/
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
Inspection,
InspectionStats,
@@ -23,19 +24,6 @@ import type {
InspectionItem,
} from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 타입 =====
interface InspectionApiItem {
id: number;
@@ -190,9 +178,9 @@ export async function getInspections(params?: {
data: Inspection[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -212,12 +200,29 @@ export async function getInspections(params?: {
console.log('[InspectionActions] GET inspections:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '검사 목록 조회에 실패했습니다.',
};
}
if (!response.ok) {
console.warn('[InspectionActions] GET inspections error:', response.status);
return {
@@ -277,9 +282,9 @@ export async function getInspectionStats(params?: {
success: boolean;
data?: InspectionStats;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.dateFrom) searchParams.set('date_from', params.dateFrom);
@@ -293,12 +298,25 @@ export async function getInspectionStats(params?: {
console.log('[InspectionActions] GET stats:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '통계 조회에 실패했습니다.',
};
}
if (!response.ok) {
console.warn('[InspectionActions] GET stats error:', response.status);
return {
@@ -341,19 +359,32 @@ export async function getInspectionById(id: string): Promise<{
success: boolean;
data?: Inspection;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}`;
console.log('[InspectionActions] GET inspection:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '검사 조회에 실패했습니다.',
};
}
if (!response.ok) {
console.error('[InspectionActions] GET inspection error:', response.status);
return {
@@ -399,10 +430,9 @@ export async function createInspection(data: {
success: boolean;
data?: Inspection;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const apiData: Record<string, unknown> = {
inspection_type: data.inspectionType,
lot_no: data.lotNo,
@@ -425,15 +455,29 @@ export async function createInspection(data: {
console.log('[InspectionActions] POST inspection request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '검사 등록에 실패했습니다.',
};
}
const result = await response.json();
console.log('[InspectionActions] POST inspection response:', result);
@@ -470,10 +514,9 @@ export async function updateInspection(
success: boolean;
data?: Inspection;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const apiData: Record<string, unknown> = {};
if (data.items) {
@@ -503,15 +546,29 @@ export async function updateInspection(
console.log('[InspectionActions] PUT inspection request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '검사 수정에 실패했습니다.',
};
}
const result = await response.json();
console.log('[InspectionActions] PUT inspection response:', result);
@@ -539,18 +596,31 @@ export async function updateInspection(
export async function deleteInspection(id: string): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}`,
{
method: 'DELETE',
headers,
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '검사 삭제에 실패했습니다.',
};
}
const result = await response.json();
console.log('[InspectionActions] DELETE inspection response:', result);
@@ -582,10 +652,9 @@ export async function completeInspection(
success: boolean;
data?: Inspection;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const apiData = {
result: data.result === '합격' ? 'pass' : 'fail',
opinion: data.opinion,
@@ -593,15 +662,29 @@ export async function completeInspection(
console.log('[InspectionActions] PATCH complete request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/inspections/${id}/complete`,
{
method: 'PATCH',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '검사 완료 처리에 실패했습니다.',
};
}
const result = await response.json();
console.log('[InspectionActions] PATCH complete response:', result);
@@ -623,4 +706,4 @@ export async function completeInspection(
error: '서버 오류가 발생했습니다.',
};
}
}
}

View File

@@ -19,7 +19,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type {
Quote,
QuoteApiData,
@@ -30,19 +30,6 @@ import type {
} from './types';
import { transformApiToFrontend, transformFrontendToApi } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 페이지네이션 타입 =====
export interface PaginationMeta {
currentPage: number;
@@ -57,9 +44,9 @@ export async function getQuotes(params?: QuoteListParams): Promise<{
data: Quote[];
pagination: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -78,12 +65,29 @@ export async function getQuotes(params?: QuoteListParams): Promise<{
console.log('[QuoteActions] GET quotes:', url);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '견적 목록 조회에 실패했습니다.',
};
}
if (!response.ok) {
console.warn('[QuoteActions] GET quotes error:', response.status);
return {
@@ -142,18 +146,29 @@ export async function getQuoteById(id: string): Promise<{
success: boolean;
data?: Quote;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
const { response, error } = await serverFetch(url, {
method: 'GET',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '견적 조회에 실패했습니다.',
};
}
if (!response.ok) {
console.error('[QuoteActions] GET quote error:', response.status);
@@ -188,21 +203,33 @@ export async function getQuoteById(id: string): Promise<{
// ===== 견적 등록 =====
export async function createQuote(
data: Partial<Quote>
): Promise<{ success: boolean; data?: Quote; error?: string }> {
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[QuoteActions] POST quote request:', apiData);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes`;
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(apiData),
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '견적 등록에 실패했습니다.',
};
}
const result = await response.json();
console.log('[QuoteActions] POST quote response:', result);
@@ -231,21 +258,33 @@ export async function createQuote(
export async function updateQuote(
id: string,
data: Partial<Quote>
): Promise<{ success: boolean; data?: Quote; error?: string }> {
): Promise<{ success: boolean; data?: Quote; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[QuoteActions] PUT quote request:', apiData);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`;
const { response, error } = await serverFetch(url, {
method: 'PUT',
body: JSON.stringify(apiData),
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '견적 수정에 실패했습니다.',
};
}
const result = await response.json();
console.log('[QuoteActions] PUT quote response:', result);
@@ -271,17 +310,28 @@ export async function updateQuote(
}
// ===== 견적 삭제 =====
export async function deleteQuote(id: string): Promise<{ success: boolean; error?: string }> {
export async function deleteQuote(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}`,
{
method: 'DELETE',
headers,
}
);
const { response, error } = await serverFetch(url, {
method: 'DELETE',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '견적 삭제에 실패했습니다.',
};
}
const result = await response.json();
console.log('[QuoteActions] DELETE quote response:', result);
@@ -304,18 +354,29 @@ export async function deleteQuote(id: string): Promise<{ success: boolean; error
}
// ===== 견적 일괄 삭제 =====
export async function bulkDeleteQuotes(ids: string[]): Promise<{ success: boolean; error?: string }> {
export async function bulkDeleteQuotes(ids: string[]): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/bulk`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/bulk`,
{
method: 'DELETE',
headers,
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
}
);
const { response, error } = await serverFetch(url, {
method: 'DELETE',
body: JSON.stringify({ ids: ids.map(id => parseInt(id, 10)) }),
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '견적 일괄 삭제에 실패했습니다.',
};
}
const result = await response.json();
console.log('[QuoteActions] BULK DELETE quotes response:', result);
@@ -342,17 +403,29 @@ export async function finalizeQuote(id: string): Promise<{
success: boolean;
data?: Quote;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/finalize`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/finalize`,
{
method: 'POST',
headers,
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '견적 확정에 실패했습니다.',
};
}
const result = await response.json();
console.log('[QuoteActions] POST finalize response:', result);
@@ -382,17 +455,29 @@ export async function cancelFinalizeQuote(id: string): Promise<{
success: boolean;
data?: Quote;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/cancel-finalize`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/cancel-finalize`,
{
method: 'POST',
headers,
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '견적 확정 취소에 실패했습니다.',
};
}
const result = await response.json();
console.log('[QuoteActions] POST cancel-finalize response:', result);
@@ -423,17 +508,29 @@ export async function convertQuoteToOrder(id: string): Promise<{
data?: Quote;
orderId?: string;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/convert`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/convert`,
{
method: 'POST',
headers,
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '수주 전환에 실패했습니다.',
};
}
const result = await response.json();
console.log('[QuoteActions] POST convert response:', result);
@@ -464,18 +561,29 @@ export async function getQuoteNumberPreview(): Promise<{
success: boolean;
data?: string;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/number/preview`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/number/preview`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
const { response, error } = await serverFetch(url, {
method: 'GET',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '견적번호 미리보기에 실패했습니다.',
};
}
const result = await response.json();
@@ -504,17 +612,29 @@ export async function generateQuotePdf(id: string): Promise<{
success: boolean;
data?: Blob;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/pdf`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/pdf`,
{
method: 'POST',
headers,
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: 'PDF 생성에 실패했습니다.',
};
}
if (!response.ok) {
const result = await response.json().catch(() => ({}));
@@ -542,18 +662,29 @@ export async function generateQuotePdf(id: string): Promise<{
export async function sendQuoteEmail(
id: string,
emailData: { email: string; subject?: string; message?: string }
): Promise<{ success: boolean; error?: string }> {
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/send/email`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/send/email`,
{
method: 'POST',
headers,
body: JSON.stringify(emailData),
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(emailData),
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '이메일 발송에 실패했습니다.',
};
}
const result = await response.json();
console.log('[QuoteActions] POST send email response:', result);
@@ -579,18 +710,29 @@ export async function sendQuoteEmail(
export async function sendQuoteKakao(
id: string,
kakaoData: { phone: string; templateId?: string }
): Promise<{ success: boolean; error?: string }> {
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/send/kakao`;
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/${id}/send/kakao`,
{
method: 'POST',
headers,
body: JSON.stringify(kakaoData),
}
);
const { response, error } = await serverFetch(url, {
method: 'POST',
body: JSON.stringify(kakaoData),
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '카카오 발송에 실패했습니다.',
};
}
const result = await response.json();
console.log('[QuoteActions] POST send kakao response:', result);
@@ -630,6 +772,7 @@ export async function getQuotesSummary(params?: {
conversionRate: number;
};
error?: string;
__authError?: boolean;
}> {
try {
// 목록 조회를 통해 통계 계산 (별도 API 없는 경우)
@@ -643,6 +786,7 @@ export async function getQuotesSummary(params?: {
return {
success: false,
error: listResult.error,
__authError: listResult.__authError,
};
}

View File

@@ -1,21 +1,8 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { ComprehensiveAnalysisData } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface TodayIssueItemApi {
id: string;
@@ -139,9 +126,9 @@ export async function getComprehensiveAnalysis(params?: {
success: boolean;
data?: ComprehensiveAnalysisData;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.date) searchParams.set('date', params.date);
@@ -149,23 +136,22 @@ export async function getComprehensiveAnalysis(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/comprehensive-analysis${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.warn('[ComprehensiveAnalysisActions] GET error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '종합 분석 조회에 실패했습니다.' };
}
const result = await response.json();
if (!result.success) {
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '종합 분석 조회에 실패했습니다.',
@@ -191,27 +177,26 @@ export async function getComprehensiveAnalysis(params?: {
export async function approveIssue(issueId: string): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${issueId}/approve`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'POST',
headers,
});
if (!response.ok) {
console.warn('[ComprehensiveAnalysisActions] POST approve error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '승인에 실패했습니다.' };
}
const result = await response.json();
if (!result.success) {
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '승인에 실패했습니다.',
@@ -232,28 +217,27 @@ export async function approveIssue(issueId: string): Promise<{
export async function rejectIssue(issueId: string, reason?: string): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/approvals/${issueId}/reject`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'POST',
headers,
body: JSON.stringify({ comment: reason }),
});
if (!response.ok) {
console.warn('[ComprehensiveAnalysisActions] POST reject error:', response.status);
return {
success: false,
error: `API 오류: ${response.status}`,
};
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '반려에 실패했습니다.' };
}
const result = await response.json();
if (!result.success) {
if (!response.ok || !result.success) {
return {
success: false,
error: result.message || '반려에 실패했습니다.',

View File

@@ -1,37 +1,37 @@
'use server';
import { cookies } from 'next/headers';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
import { serverFetch } from '@/lib/api/fetch-wrapper';
// ===== 계정 탈퇴 =====
export async function withdrawAccount(): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/users/withdraw`,
{
method: 'POST',
headers,
body: JSON.stringify({}),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '계정 탈퇴에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -55,19 +55,32 @@ export async function withdrawAccount(): Promise<{
export async function suspendTenant(): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants/suspend`,
{
method: 'POST',
headers,
body: JSON.stringify({}),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '사용 중지에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {

View File

@@ -1,6 +1,6 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { Account, AccountFormData, AccountStatus } from './types';
import { BANK_LABELS } from './types';
@@ -39,19 +39,6 @@ interface ApiSingleResponse {
data: BankAccountApiData;
}
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 데이터 변환: API → Frontend =====
function transformApiToFrontend(apiData: BankAccountApiData): Account {
return {
@@ -91,9 +78,9 @@ export async function getBankAccounts(params?: {
data?: Account[];
meta?: PaginationMeta;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', params.page.toString());
@@ -102,12 +89,23 @@ export async function getBankAccounts(params?: {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts?${searchParams.toString()}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return { success: false, error: '계좌 목록 조회에 실패했습니다.' };
}
const result: ApiListResponse = await response.json();
if (!response.ok || !result.success) {
@@ -127,18 +125,29 @@ export async function getBankAccount(id: number): Promise<{
success: boolean;
data?: Account;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return { success: false, error: '계좌 조회에 실패했습니다.' };
}
const result: ApiSingleResponse = await response.json();
if (!response.ok || !result.success) {
@@ -158,20 +167,31 @@ export async function createBankAccount(data: AccountFormData): Promise<{
success: boolean;
data?: Account;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return { success: false, error: '계좌 등록에 실패했습니다.' };
}
const result: ApiSingleResponse = await response.json();
if (!response.ok || !result.success) {
@@ -194,20 +214,31 @@ export async function updateBankAccount(
success: boolean;
data?: Account;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return { success: false, error: '계좌 수정에 실패했습니다.' };
}
const result: ApiSingleResponse = await response.json();
if (!response.ok || !result.success) {
@@ -226,17 +257,28 @@ export async function updateBankAccount(
export async function deleteBankAccount(id: number): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}`,
{
method: 'DELETE',
headers,
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return { success: false, error: '계좌 삭제에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -255,17 +297,28 @@ export async function toggleBankAccountStatus(id: number): Promise<{
success: boolean;
data?: Account;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}/toggle`,
{
method: 'PATCH',
headers,
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return { success: false, error: '상태 변경에 실패했습니다.' };
}
const result: ApiSingleResponse = await response.json();
if (!response.ok || !result.success) {
@@ -285,17 +338,28 @@ export async function setPrimaryBankAccount(id: number): Promise<{
success: boolean;
data?: Account;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/bank-accounts/${id}/set-primary`,
{
method: 'PATCH',
headers,
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return { success: false, error: '대표 계좌 설정에 실패했습니다.' };
}
const result: ApiSingleResponse = await response.json();
if (!response.ok || !result.success) {

View File

@@ -1,20 +1,6 @@
'use server';
import { cookies } from 'next/headers';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://sam.kr:8080';
// ===== API Helper =====
async function getAuthHeaders() {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Content-Type': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
...(token && { Authorization: `Bearer ${token}` }),
};
}
import { serverFetch } from '@/lib/api/fetch-wrapper';
// ===== 타입 정의 =====
@@ -79,21 +65,30 @@ export async function getAttendanceSetting(): Promise<{
success: boolean;
data?: AttendanceSettingFormData;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getAuthHeaders();
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/attendance`,
{
method: 'GET',
cache: 'no-store',
}
);
const response = await fetch(`${API_BASE_URL}/api/v1/settings/attendance`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
if (error) {
return {
success: false,
error: errorData.message || `API 오류: ${response.status}`,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
const errorData = await response?.json().catch(() => ({}));
return {
success: false,
error: errorData?.message || `API 오류: ${response?.status}`,
};
}
@@ -121,21 +116,30 @@ export async function updateAttendanceSetting(
success: boolean;
data?: AttendanceSettingFormData;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getAuthHeaders();
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/attendance`,
{
method: 'PUT',
body: JSON.stringify(transformToApi(data)),
}
);
const response = await fetch(`${API_BASE_URL}/api/v1/settings/attendance`, {
method: 'PUT',
headers,
body: JSON.stringify(transformToApi(data)),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
if (error) {
return {
success: false,
error: errorData.message || `API 오류: ${response.status}`,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
const errorData = await response?.json().catch(() => ({}));
return {
success: false,
error: errorData?.message || `API 오류: ${response?.status}`,
};
}

View File

@@ -1,6 +1,6 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { CompanyFormData } from './types';
// API 응답 타입
@@ -32,19 +32,6 @@ interface TenantApiData {
updated_at?: string;
}
// API 헤더 생성
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
/**
* 테넌트 정보 조회
*/
@@ -52,18 +39,29 @@ export async function getCompanyInfo(): Promise<{
success: boolean;
data?: CompanyFormData & { tenantId: number };
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return { success: false, error: '회사 정보 조회에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -90,20 +88,31 @@ export async function updateCompanyInfo(
success: boolean;
data?: CompanyFormData;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(tenantId, data);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/tenants`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return { success: false, error: '회사 정보 수정에 실패했습니다.' };
}
const result = await response.json();
if (!response.ok || !result.success) {

View File

@@ -1,21 +1,8 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { LeavePolicySettings } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== API 응답 타입 =====
interface LeavePolicyApi {
id: number;
@@ -70,22 +57,29 @@ export async function getLeavePolicy(): Promise<{
success: boolean;
data?: LeavePolicySettings;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/leave-policy`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
console.warn('[LeavePolicyActions] GET error:', response.status);
if (error) {
return {
success: false,
error: `API 오류: ${response.status}`,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
console.warn('[LeavePolicyActions] GET error:', response?.status);
return {
success: false,
error: `API 오류: ${response?.status}`,
};
}
@@ -118,24 +112,30 @@ export async function updateLeavePolicy(data: Partial<LeavePolicySettings>): Pro
success: boolean;
data?: LeavePolicySettings;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/leave-policy`;
const apiData = transformToApi(data);
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'PUT',
headers,
body: JSON.stringify(apiData),
});
if (!response.ok) {
console.warn('[LeavePolicyActions] PUT error:', response.status);
if (error) {
return {
success: false,
error: `API 오류: ${response.status}`,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
console.warn('[LeavePolicyActions] PUT error:', response?.status);
return {
success: false,
error: `API 오류: ${response?.status}`,
};
}

View File

@@ -1,42 +1,36 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { NotificationSettings } from './types';
import { DEFAULT_NOTIFICATION_SETTINGS } from './types';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 알림 설정 조회 =====
export async function getNotificationSettings(): Promise<{
success: boolean;
data: NotificationSettings;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/notifications`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (!response.ok) {
console.warn('[NotificationActions] GET settings error:', response.status);
if (error) {
return {
success: false,
data: DEFAULT_NOTIFICATION_SETTINGS,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
console.warn('[NotificationActions] GET settings error:', response?.status);
return {
success: true,
data: DEFAULT_NOTIFICATION_SETTINGS,
@@ -69,20 +63,33 @@ export async function getNotificationSettings(): Promise<{
// ===== 알림 설정 저장 =====
export async function saveNotificationSettings(
settings: NotificationSettings
): Promise<{ success: boolean; error?: string }> {
): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(settings);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/settings/notifications`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '알림 설정 저장에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {

View File

@@ -1,22 +1,9 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { PaymentApiData, PaymentHistory } from './types';
import { transformApiToFrontend } from './utils';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 결제 목록 조회 =====
export async function getPayments(params?: {
page?: number;
@@ -35,10 +22,9 @@ export async function getPayments(params?: {
total: number;
};
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
// 쿼리 파라미터 생성
const searchParams = new URLSearchParams();
if (params?.page) searchParams.append('page', String(params.page));
@@ -51,12 +37,30 @@ export async function getPayments(params?: {
const queryString = searchParams.toString();
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/payments${queryString ? `?${queryString}` : ''}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: [],
pagination: { currentPage: 1, lastPage: 1, perPage: 20, total: 0 },
error: '결제 내역을 불러오는데 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -144,19 +148,32 @@ export async function getPaymentStatement(id: string): Promise<{
total: number;
};
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/payments/${id}/statement`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '명세서를 불러오는데 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {

View File

@@ -11,7 +11,7 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { Popup, PopupFormData } from './types';
import { transformApiToFrontend, transformFrontendToApi, type PopupApiData } from './utils';
@@ -25,25 +25,6 @@ interface ApiResponse<T> {
message: string;
}
// ============================================
// 헬퍼 함수
// ============================================
/**
* API 헤더 생성
*/
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ============================================
// API 함수
// ============================================
@@ -65,7 +46,6 @@ export async function getPopups(params?: {
status?: string;
}): Promise<Popup[]> {
try {
const headers = await getApiHeaders();
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
@@ -76,12 +56,16 @@ export async function getPopups(params?: {
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups?${searchParams.toString()}`;
const response = await fetch(url, {
const { response, error } = await serverFetch(url, {
method: 'GET',
headers,
cache: 'no-store',
});
if (error || !response) {
console.error('[PopupActions] GET list error:', error?.message);
return [];
}
if (!response.ok) {
console.error('[PopupActions] GET list error:', response.status);
return [];
@@ -106,17 +90,19 @@ export async function getPopups(params?: {
*/
export async function getPopupById(id: string): Promise<Popup | null> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups/${id}`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error || !response) {
console.error('[PopupActions] GET popup error:', error?.message);
return null;
}
if (!response.ok) {
console.error('[PopupActions] GET popup error:', response.status);
return null;
@@ -140,22 +126,35 @@ export async function getPopupById(id: string): Promise<Popup | null> {
*/
export async function createPopup(
data: PopupFormData
): Promise<{ success: boolean; data?: Popup; error?: string }> {
): Promise<{ success: boolean; data?: Popup; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[PopupActions] POST popup request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups`,
{
method: 'POST',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '팝업 등록에 실패했습니다.',
};
}
const result = await response.json();
console.log('[PopupActions] POST popup response:', result);
@@ -185,22 +184,35 @@ export async function createPopup(
export async function updatePopup(
id: string,
data: PopupFormData
): Promise<{ success: boolean; data?: Popup; error?: string }> {
): Promise<{ success: boolean; data?: Popup; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const apiData = transformFrontendToApi(data);
console.log('[PopupActions] PUT popup request:', apiData);
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups/${id}`,
{
method: 'PUT',
headers,
body: JSON.stringify(apiData),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '팝업 수정에 실패했습니다.',
};
}
const result = await response.json();
console.log('[PopupActions] PUT popup response:', result);
@@ -227,18 +239,30 @@ export async function updatePopup(
/**
* 팝업 삭제
*/
export async function deletePopup(id: string): Promise<{ success: boolean; error?: string }> {
export async function deletePopup(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean }> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/popups/${id}`,
{
method: 'DELETE',
headers,
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '팝업 삭제에 실패했습니다.',
};
}
const result = await response.json();
console.log('[PopupActions] DELETE popup response:', result);

View File

@@ -1,40 +1,42 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
import type { SubscriptionApiData, UsageApiData, SubscriptionInfo } from './types';
import { transformApiToFrontend } from './utils';
// ===== API 헤더 생성 =====
async function getApiHeaders(): Promise<HeadersInit> {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
'X-API-KEY': process.env.API_KEY || '',
};
}
// ===== 현재 활성 구독 조회 =====
export async function getCurrentSubscription(): Promise<{
success: boolean;
data: SubscriptionApiData | null;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/current`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error) {
return {
success: false,
data: null,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: null,
error: '구독 정보를 불러오는데 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -64,19 +66,34 @@ export async function getUsage(): Promise<{
success: boolean;
data: UsageApiData | null;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/usage`,
{
method: 'GET',
headers,
cache: 'no-store',
}
);
if (error) {
return {
success: false,
data: null,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
data: null,
error: '사용량 정보를 불러오는데 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -108,19 +125,32 @@ export async function cancelSubscription(
): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/${id}/cancel`,
{
method: 'POST',
headers,
body: JSON.stringify({ reason }),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '구독 취소에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {
@@ -147,19 +177,32 @@ export async function requestDataExport(
success: boolean;
data?: { id: number; status: string };
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getApiHeaders();
const response = await fetch(
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/subscriptions/export`,
{
method: 'POST',
headers,
body: JSON.stringify({ export_type: exportType }),
}
);
if (error) {
return {
success: false,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response) {
return {
success: false,
error: '내보내기 요청에 실패했습니다.',
};
}
const result = await response.json();
if (!response.ok || !result.success) {

View File

@@ -1,21 +1,9 @@
'use server';
import { cookies } from 'next/headers';
import { serverFetch } from '@/lib/api/fetch-wrapper';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://sam.kr:8080';
// ===== API Helper =====
async function getAuthHeaders() {
const cookieStore = await cookies();
const token = cookieStore.get('access_token')?.value;
return {
'Content-Type': 'application/json',
'X-API-KEY': process.env.NEXT_PUBLIC_API_KEY || '',
...(token && { Authorization: `Bearer ${token}` }),
};
}
// ===== 타입 정의 =====
// API 응답 타입
@@ -99,21 +87,27 @@ export async function getWorkSetting(): Promise<{
success: boolean;
data?: WorkSettingFormData;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_BASE_URL}/api/v1/settings/work`, {
const { response, error } = await serverFetch(`${API_BASE_URL}/api/v1/settings/work`, {
method: 'GET',
headers,
cache: 'no-store',
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
if (error) {
return {
success: false,
error: errorData.message || `API 오류: ${response.status}`,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
const errorData = await response?.json().catch(() => ({}));
return {
success: false,
error: errorData?.message || `API 오류: ${response?.status}`,
};
}
@@ -141,21 +135,27 @@ export async function updateWorkSetting(
success: boolean;
data?: WorkSettingFormData;
error?: string;
__authError?: boolean;
}> {
try {
const headers = await getAuthHeaders();
const response = await fetch(`${API_BASE_URL}/api/v1/settings/work`, {
const { response, error } = await serverFetch(`${API_BASE_URL}/api/v1/settings/work`, {
method: 'PUT',
headers,
body: JSON.stringify(transformToApi(data)),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
if (error) {
return {
success: false,
error: errorData.message || `API 오류: ${response.status}`,
error: error.message,
__authError: error.code === 'UNAUTHORIZED',
};
}
if (!response || !response.ok) {
const errorData = await response?.json().catch(() => ({}));
return {
success: false,
error: errorData?.message || `API 오류: ${response?.status}`,
};
}