diff --git a/Frontend/luckeyseven/src/components/Tabs.js b/Frontend/luckeyseven/src/components/Tabs.js index c315375c..860b8a1b 100644 --- a/Frontend/luckeyseven/src/components/Tabs.js +++ b/Frontend/luckeyseven/src/components/Tabs.js @@ -1,21 +1,22 @@ import React from 'react'; -import { Link } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; -import { currentTeamIdState } from '../recoil/atoms/teamAtoms'; +import {useRecoilValue} from 'recoil'; +import {currentTeamIdState} from '../recoil/atoms/teamAtoms'; import styles from '../styles/Tabs.module.css'; -const Tabs = ({ activeTab, setActiveTab }) => { +const Tabs = ({activeTab, setActiveTab}) => { const teamId = useRecoilValue(currentTeamIdState); const tabs = ['Overview', 'Members', 'Expenses', 'Settlement']; const getTabPath = (tab) => { - if (!teamId) return '#'; // Or handle the case where teamId is not available + if (!teamId) { + return '#'; + } // Or handle the case where teamId is not available switch (tab) { case 'Expenses': return `/teams/${teamId}/expenses`; - // case 'Settlement': - // return `/teams/${teamId}/settlements`; + // case 'Settlement': + // return `/teams/${teamId}/settlements`; default: return '#'; // Overview and Members will still use setActiveTab } @@ -27,14 +28,16 @@ const Tabs = ({ activeTab, setActiveTab }) => { const isLink = tab === 'Expenses'; const path = getTabPath(tab); return ( - setActiveTab(tab)} // Keep active tab state for styling - > - {tab} - + ); })} diff --git a/Frontend/luckeyseven/src/components/settlement/settlement-detail.jsx b/Frontend/luckeyseven/src/components/settlement/settlement-detail.jsx index 2f498916..eb98a931 100644 --- a/Frontend/luckeyseven/src/components/settlement/settlement-detail.jsx +++ b/Frontend/luckeyseven/src/components/settlement/settlement-detail.jsx @@ -24,7 +24,8 @@ export function SettlementDetail({settlement: initialSettlement}) {
-

정산 #{String(settlement.id).substring(0, +

정산 #{String(settlement.id).substring( + 0, 8)}

diff --git a/Frontend/luckeyseven/src/components/settlement/settlement-form.jsx b/Frontend/luckeyseven/src/components/settlement/settlement-form.jsx index e1e06f35..db6f74c0 100644 --- a/Frontend/luckeyseven/src/components/settlement/settlement-form.jsx +++ b/Frontend/luckeyseven/src/components/settlement/settlement-form.jsx @@ -13,7 +13,9 @@ export function SettlementForm({ settlement, users, expenses, - isEditing = false + isEditing = false, + onFormSubmit, + onCancel }) { const navigate = useNavigate() const {addToast} = useToast() @@ -104,6 +106,12 @@ export function SettlementForm({ : "새로운 정산 내역이 생성되었습니다.", }) + // onFormSubmit이 있으면 호출 (모달에서 사용할 때) + if (onFormSubmit) { + onFormSubmit(result) + return + } + // 수정 완료 후 상세 페이지로 이동 navigate(`/settlements/${isEditing ? settlement.id : result.id}`) } catch (error) { @@ -118,6 +126,14 @@ export function SettlementForm({ } } + const handleCancel = () => { + if (onCancel) { + onCancel() + } else { + navigate(-1) + } + } + // 선택된 지출 항목 정보 const selectedExpense = formData.expenseId ? expenses.find( (expense) => expense.id === formData.expenseId) : null @@ -238,7 +254,7 @@ export function SettlementForm({
+ inline={true} + onClick={(e) => e.stopPropagation()}/> +
- + ))} ) -} +} \ No newline at end of file diff --git a/Frontend/luckeyseven/src/pages/ExpenseDialog/AddExpenseDialog.jsx b/Frontend/luckeyseven/src/pages/ExpenseDialog/AddExpenseDialog.jsx index 5a546685..4fe38aff 100644 --- a/Frontend/luckeyseven/src/pages/ExpenseDialog/AddExpenseDialog.jsx +++ b/Frontend/luckeyseven/src/pages/ExpenseDialog/AddExpenseDialog.jsx @@ -1,7 +1,13 @@ -import React, { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; -import { getTeamMembers, createExpense } from '../../service/ExpenseService'; +import React, {useEffect, useState} from 'react'; +import {useParams} from 'react-router-dom'; +import {createExpense, getTeamMembers} from '../../service/ExpenseService'; import '../../components/styles/addExpenseDialog.css'; +import {useRecoilValue} from "recoil"; +import { + currentTeamIdState, + teamForeignCurrencyState +} from "../../recoil/atoms/teamAtoms"; + const CATEGORY_LABELS = { MEAL: '식사', SNACK: '간식', @@ -16,8 +22,12 @@ const PAYMENT_LABELS = { }; const categories = Object.keys(CATEGORY_LABELS); const paymentMethods = Object.keys(PAYMENT_LABELS); -export default function AddExpenseDialog({ onClose, onSuccess }) { - const { teamId } = useParams(); + +export default function AddExpenseDialog({onClose, onSuccess}) { + const recoilTeamId = useRecoilValue(currentTeamIdState); + const foreignCurrency = useRecoilValue(teamForeignCurrencyState) || 'USD'; // 외화 통화 단위 가져오기 + const paramTeamId = useParams().teamId; + const teamId = recoilTeamId || paramTeamId; const [users, setUsers] = useState([]); const [form, setForm] = useState({ description: '', @@ -27,6 +37,14 @@ export default function AddExpenseDialog({ onClose, onSuccess }) { paymentMethod: paymentMethods[0], settlerIds: [] }); + + // 결제 수단에 따른 통화 단위 + const CURRENCY_LABELS = { + CARD: 'KRW', + CASH: foreignCurrency, + OTHER: '', + }; + // ESC 키로 닫기 useEffect(() => { const handleKeyDown = (e) => { @@ -37,6 +55,7 @@ export default function AddExpenseDialog({ onClose, onSuccess }) { document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [onClose]); + useEffect(() => { async function fetchMembers() { try { @@ -55,23 +74,30 @@ export default function AddExpenseDialog({ onClose, onSuccess }) { alert('팀 멤버 로딩에 실패했습니다. 다시 시도해주세요.'); } } + fetchMembers(); }, [teamId]); + const handleChange = (e) => { - const { name, value, options } = e.target; + const {name, value, options} = e.target; + if (name === 'settlerIds') { const selected = Array.from(options) - .filter(o => o.selected) - .map(o => o.value); - setForm(f => ({ ...f, settlerIds: selected })); + .filter(o => o.selected) + .map(o => o.value); + setForm(f => ({...f, settlerIds: selected})); } else if (name === 'amount') { - setForm(f => ({ ...f, amount: Number(value) })); + setForm(f => ({...f, amount: Number(value)})); } else if (name === 'payerId') { - setForm(f => ({ ...f, payerId: value })); + setForm(f => ({...f, payerId: value})); + } else if (name === 'paymentMethod') { + // 결제 수단이 변경되면 금액을 초기화 + setForm(f => ({...f, paymentMethod: value, amount: ''})); } else { - setForm(f => ({ ...f, [name]: value })); + setForm(f => ({...f, [name]: value})); } }; + const handleSubmit = async (e) => { e.preventDefault(); try { @@ -100,7 +126,8 @@ export default function AddExpenseDialog({ onClose, onSuccess }) { foreignBalance: result.foreignBalance }); } else { - alert(`등록 완료!\n잔고: ₩${result.balance}\n해외잔고: $${result.foreignBalance}`); + alert( + `등록 완료!\n잔고: ₩${result.balance}\n해외잔고: ${result.foreignBalance} ${foreignCurrency}`); } onClose(); } catch (err) { @@ -108,87 +135,118 @@ export default function AddExpenseDialog({ onClose, onSuccess }) { alert(msg); } }; + + // 현재 선택된 결제 수단에 따른 통화 단위 + const currencyUnit = CURRENCY_LABELS[form.paymentMethod] || ''; + return ( -
-
-
-

새 지출 추가

- -
-
- - - - - - -
- - -
-
+
+
+
+

새 지출 추가

+ +
+
+ + + + + + +
+ + +
+
+
-
); } \ No newline at end of file diff --git a/Frontend/luckeyseven/src/pages/ExpenseDialog/ExpenseDetailDialog.jsx b/Frontend/luckeyseven/src/pages/ExpenseDialog/ExpenseDetailDialog.jsx index 5a5ade83..2930f1e3 100644 --- a/Frontend/luckeyseven/src/pages/ExpenseDialog/ExpenseDetailDialog.jsx +++ b/Frontend/luckeyseven/src/pages/ExpenseDialog/ExpenseDetailDialog.jsx @@ -5,6 +5,8 @@ import { updateExpense } from '../../service/ExpenseService'; import '../../components/styles/expenseDetailDialog.css'; +import {useRecoilValue} from "recoil"; +import {teamForeignCurrencyState} from "../../recoil/atoms/teamAtoms"; const CATEGORY_LABELS = { MEAL: '식사', @@ -26,6 +28,7 @@ export default function ExpenseDetailDialog({ onUpdate, onDelete }) { + const foreignCurrency = useRecoilValue(teamForeignCurrencyState) || 'USD'; // 외화 통화 단위 가져오기 const [detail, setDetail] = useState(null); const [isEditing, setIsEditing] = useState(false); const [formData, setFormData] = useState({ @@ -34,6 +37,13 @@ export default function ExpenseDetailDialog({ category: '' }); + // 결제 수단에 따른 통화 단위 + const CURRENCY_LABELS = { + CARD: 'KRW', + CASH: foreignCurrency, + OTHER: '', + }; + // ESC 키로 닫기 useEffect(() => { const handleKeyDown = (e) => { @@ -133,6 +143,9 @@ export default function ExpenseDetailDialog({ setIsEditing(false); }; + // 현재 선택된 결제 수단에 따른 통화 단위 + const currencyUnit = CURRENCY_LABELS[detail.paymentMethod] || ''; + return (
@@ -154,14 +167,36 @@ export default function ExpenseDetailDialog({ />
- - + +
+ + {currencyUnit && ( + + {currencyUnit} + + )} +
@@ -179,14 +214,19 @@ export default function ExpenseDetailDialog({ ) : ( <>

설명 {detail.description}

-

지출 금액 {detail.amount.toLocaleString()} +

+ 지출 금액 + + {detail.amount.toLocaleString()} + {currencyUnit && ` ${currencyUnit}`} +

카테고리{' '}{CATEGORY_LABELS[detail.category]}

결제 수단{' '}{PAYMENT_LABELS[detail.paymentMethod]} + data-payment={detail.paymentMethod}>{PAYMENT_LABELS[detail.paymentMethod]} {currencyUnit + && `(${currencyUnit})`}

결제자 {detail.payerNickname}

결제일 {fmtDate(detail.createdAt)}

diff --git a/Frontend/luckeyseven/src/pages/ExpenseDialog/ExpenseList.jsx b/Frontend/luckeyseven/src/pages/ExpenseDialog/ExpenseList.jsx index 6b0a813a..cb48455f 100644 --- a/Frontend/luckeyseven/src/pages/ExpenseDialog/ExpenseList.jsx +++ b/Frontend/luckeyseven/src/pages/ExpenseDialog/ExpenseList.jsx @@ -1,14 +1,18 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; +import React, {useCallback, useEffect, useState} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {useRecoilValue} from 'recoil'; import AddExpenseDialog from './AddExpenseDialog'; import ExpenseDetailDialog from './ExpenseDetailDialog'; import Header from '../../components/Header'; -import { getListExpense } from '../../service/ExpenseService'; -import { currentTeamIdState } from '../../recoil/atoms/teamAtoms'; -import { FaMoneyBillWave } from 'react-icons/fa'; -import { FiHome, FiPlus } from 'react-icons/fi'; +import {getListExpense} from '../../service/ExpenseService'; +import { + currentTeamIdState, + teamForeignCurrencyState +} from '../../recoil/atoms/teamAtoms'; +import {FaMoneyBillWave} from 'react-icons/fa'; +import {FiPlus} from 'react-icons/fi'; import '../../components/styles/expenseList.css'; + const CATEGORY_LABELS = { MEAL: '식사', SNACK: '간식', @@ -16,8 +20,17 @@ const CATEGORY_LABELS = { ACCOMMODATION: '숙박', MISCELLANEOUS: '기타', }; + +// 결제 수단에 따른 통화 단위 매핑 +const PAYMENT_METHOD_TO_CURRENCY = { + CARD: 'KRW', + CASH: '', // 실제 외화 통화 단위는 동적으로 결정됨 + OTHER: '', +}; + export default function ExpenseList() { const teamId = useRecoilValue(currentTeamIdState); + const foreignCurrency = useRecoilValue(teamForeignCurrencyState) || 'USD'; // 외화 통화 단위 가져오기 const navigate = useNavigate(); const [expenses, setExpenses] = useState([]); const [page, setPage] = useState(0); @@ -27,13 +40,15 @@ export default function ExpenseList() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [balances, setBalances] = useState(null); - const [notification, setNotification] = useState({ message: '', type: '' }); + const [notification, setNotification] = useState({message: '', type: ''}); const [showAddDialog, setShowAddDialog] = useState(false); const [selectedExpenseId, setSelectedExpenseId] = useState(null); + const fetchExpenses = useCallback(async () => { setLoading(true); try { - const data = await getListExpense(teamId, page, size, `createdAt,${sortDirection}`); + const data = await getListExpense(teamId, page, size, + `createdAt,${sortDirection}`); setExpenses(data.content); setTotalPages(data.totalPages); setError(null); @@ -43,185 +58,228 @@ export default function ExpenseList() { setLoading(false); } }, [teamId, page, size, sortDirection]); + useEffect(() => { fetchExpenses(); }, [fetchExpenses]); + useEffect(() => { - if (!balances && !notification.message) return; + if (!balances && !notification.message) { + return; + } const timer = setTimeout(() => { setBalances(null); - setNotification({ message: '', type: '' }); + setNotification({message: '', type: ''}); }, 10000); return () => clearTimeout(timer); }, [balances, notification]); + const fmt = v => (v != null ? v.toLocaleString() : '-'); + const formatDate = d => - new Date(d).toLocaleDateString('ko-KR', { - year: 'numeric', - month: 'long', - day: 'numeric', - weekday: 'short', - }); + new Date(d).toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'long', + day: 'numeric', + weekday: 'short', + }); + + // 결제 수단에 따라 통화 단위 반환 + const getCurrencyUnit = (paymentMethod) => { + if (paymentMethod === 'CARD') { + return 'KRW'; + } + if (paymentMethod === 'CASH') { + return foreignCurrency; + } + return ''; + }; + const openDetail = id => setSelectedExpenseId(id); const closeDetail = () => setSelectedExpenseId(null); const goToPage = n => setPage(n - 1); + const handleAddSuccess = async (_, bal) => { setBalances(bal); - setNotification({ message: '지출이 성공적으로 등록되었습니다.', type: 'register' }); + setNotification({message: '지출이 성공적으로 등록되었습니다.', type: 'register'}); setShowAddDialog(false); await fetchExpenses(); }; + const handleUpdateSuccess = (updatedExpense, bal) => { - setExpenses(prev => prev.map(e => (e.id === updatedExpense.id ? updatedExpense : e))); + setExpenses(prev => prev.map( + e => (e.id === updatedExpense.id ? updatedExpense : e))); setBalances(bal); - setNotification({ message: '지출이 성공적으로 수정되었습니다.', type: 'update' }); + setNotification({message: '지출이 성공적으로 수정되었습니다.', type: 'update'}); closeDetail(); }; + const handleDeleteSuccess = (deletedId, bal) => { setExpenses(prev => prev.filter(e => e.id !== deletedId)); setBalances(bal); - setNotification({ message: '지출이 성공적으로 삭제되었습니다.', type: 'delete' }); + setNotification({message: '지출이 성공적으로 삭제되었습니다.', type: 'delete'}); closeDetail(); }; + if (loading) { return ( -
-
-
-
데이터를 불러오고 있습니다...
+
+
+
+
데이터를 불러오고 있습니다...
+
-
); } + if (error) { return ( -
-
-
-
-

데이터를 불러오는 중 오류가 발생했습니다

-

{error.message}

+
+
+
+

데이터를 불러오는 중 오류가 발생했습니다

+

{error.message}

+
-
); } + return ( -
-
-
-

- 지출 내역 -

- {/* 잔고/알림 배너 */} - {(balances || notification.message) && ( -
- {balances && ( -
- 원화 잔고: - ₩{fmt(balances.balance)}   - 외화 잔고: - ${fmt(balances.foreignBalance)} -
- )} - {notification.message && ( -
- {notification.message} +
+
+

+ 지출 내역 +

+ {/* 잔고/알림 배너 */} + {(balances || notification.message) && ( +
+ {balances && ( +
+ 원화 잔고: + ₩{fmt(balances.balance)}   + 외화 잔고: + {fmt( + balances.foreignBalance)} {foreignCurrency} +
+ )} + {notification.message && ( +
+ {notification.message} +
+ )}
- )} -
- )} - {/* 액션 바 */} -
-
- {/* */} - -
-
- -
-
- {/* 테이블 / 빈 상태 */} - {expenses.length === 0 ? ( -
-

지출 내역이 없습니다

-

'지출 추가' 버튼을 클릭하여 첫 지출을 등록해보세요.

+ )} + {/* 액션 바 */} +
+
+ +
+
+ +
- ) : ( -
- - - - - - - - - - - - {expenses.map(exp => ( - openDetail(exp.id)} style={{ cursor: 'pointer' }}> - - - - - + {/* 테이블 / 빈 상태 */} + {expenses.length === 0 ? ( +
+

지출 내역이 없습니다

+

'지출 추가' 버튼을 클릭하여 첫 지출을 등록해보세요.

+
+ ) : ( +
+
제목가격 (KRW, USD 등)카테고리날짜결제자
{exp.description}{exp.amount.toLocaleString()} - - {CATEGORY_LABELS[exp.category] || exp.category} - - {formatDate(exp.createdAt)}{exp.payerNickname}
+ + + + + + + - ))} - -
제목가격카테고리날짜결제자
-
- )} - {/* 페이지네이션 */} - {totalPages > 1 && ( -
- - {Array.from({ length: totalPages }, (_, i) => { - const p = i + 1, - cur = page + 1; - if (p === 1 || p === totalPages || (p >= cur - 1 && p <= cur + 1)) { - return ( - - ); - } - if (p === cur - 2 && cur > 3) return ...; - if (p === cur + 2 && cur < totalPages - 2) return ...; - return null; - })} - -
- )} - {/* 다이얼로그 */} - {showAddDialog && setShowAddDialog(false)} onSuccess={handleAddSuccess} />} - {selectedExpenseId && ( - - )} + + + {expenses.map(exp => { + const currencyUnit = getCurrencyUnit(exp.paymentMethod); + return ( + openDetail(exp.id)} + style={{cursor: 'pointer'}}> + {exp.description} + + {exp.amount.toLocaleString()} {currencyUnit + && `${currencyUnit}`} + + + + {CATEGORY_LABELS[exp.category] || exp.category} + + + {formatDate(exp.createdAt)} + {exp.payerNickname} + + ); + })} + + +
+ )} + {/* 페이지네이션 */} + {totalPages > 1 && ( +
+ + {Array.from({length: totalPages}, (_, i) => { + const p = i + 1, + cur = page + 1; + if (p === 1 || p === totalPages || (p >= cur - 1 && p <= cur + + 1)) { + return ( + + ); + } + if (p === cur - 2 && cur > 3) { + return ...; + } + if (p === cur + 2 && cur < totalPages - 2) { + return ...; + } + return null; + })} + +
+ )} + {/* 다이얼로그 */} + {showAddDialog && setShowAddDialog(false)} + onSuccess={handleAddSuccess}/>} + {selectedExpenseId && ( + + )} +
-
); } \ No newline at end of file diff --git a/Frontend/luckeyseven/src/pages/Settlement/TeamSettlementsPage.jsx b/Frontend/luckeyseven/src/pages/Settlement/TeamSettlementsPage.jsx index bbf51970..d58b1c20 100644 --- a/Frontend/luckeyseven/src/pages/Settlement/TeamSettlementsPage.jsx +++ b/Frontend/luckeyseven/src/pages/Settlement/TeamSettlementsPage.jsx @@ -4,11 +4,18 @@ import {useEffect, useState} from "react" import {useLocation, useNavigate, useParams} from "react-router-dom" import {SettlementList} from "../../components/settlement/settlement-list" import {SettlementFilter} from "../../components/settlement/settlement-filter" -import {getListSettlements, getUsers} from "../../service/settlementService" +import { + getListSettlements, + getSettlementById, + getUsers, + updateSettlement +} from "../../service/settlementService" import {useToast} from "../../context/ToastContext" import {getAllExpense} from "../../service/ExpenseService"; import {useRecoilValue} from "recoil"; import {currentTeamIdState} from "../../recoil/atoms/teamAtoms"; +import {SettlementForm} from "../../components/settlement/settlement-form" +import {StatusBadge} from "../../components/common/StatusBadge"; export function TeamSettlementsPage() { const recoilTeamId = useRecoilValue(currentTeamIdState) @@ -24,6 +31,13 @@ export function TeamSettlementsPage() { const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) + // 모달 관련 상태 추가 + const [isModalOpen, setIsModalOpen] = useState(false) + const [selectedSettlement, setSelectedSettlement] = useState(null) + const [isModalLoading, setIsModalLoading] = useState(false) + const [modalMode, setModalMode] = useState("detail") // "detail" 또는 "edit" + const [isActionLoading, setIsActionLoading] = useState(false) + // 페이징 관련 상태 const [totalPages, setTotalPages] = useState(0) const [totalElements, setTotalElements] = useState(0) @@ -57,6 +71,111 @@ export function TeamSettlementsPage() { navigate(`${location.pathname}?${params.toString()}`) } + // 정산 항목 클릭 핸들러 + const handleSettlementClick = async (settlementId) => { + try { + setIsModalLoading(true) + setIsModalOpen(true) + setModalMode("detail") + const settlement = await getSettlementById(settlementId) + setSelectedSettlement(settlement) + } catch (error) { + console.error("정산 내역 조회 오류:", error) + addToast({ + title: "오류 발생", + description: "정산 내역을 불러오는데 실패했습니다.", + variant: "destructive", + }) + } finally { + setIsModalLoading(false) + } + } + + // 모달 닫기 핸들러 + const handleCloseModal = () => { + setIsModalOpen(false) + setSelectedSettlement(null) + setModalMode("detail") + } + + // 정산 편집 모드로 전환 + const handleEditMode = () => { + setModalMode("edit") + } + + // 정산 상태 변경 (완료/취소) + const handleSettlementStatusChange = async () => { + if (!selectedSettlement) { + return + } + + try { + setIsActionLoading(true) + const newStatus = !selectedSettlement.isSettled + + // updateSettlement 함수 호출 (3번째 매개변수가 toggleSettled를 의미) + const updatedSettlement = await updateSettlement( + selectedSettlement.id, + {}, + true + ) + + // 토스트 메시지 표시 + addToast({ + title: newStatus ? "정산 완료" : "정산 완료 취소", + description: newStatus ? "정산이 완료 처리되었습니다." : "정산 완료가 취소되었습니다.", + }) + + // 선택된 정산 업데이트 + const updated = { + ...selectedSettlement, + isSettled: newStatus, + updatedAt: updatedSettlement.updatedAt || new Date().toISOString() + } + setSelectedSettlement(updated) + + // 목록에서도 업데이트 + setSettlements(prevSettlements => { + return { + ...prevSettlements, + content: prevSettlements.content.map(item => + item.id === updated.id ? updated : item + ) + } + }) + } catch (error) { + console.error("정산 상태 변경 오류:", error) + addToast({ + title: "오류 발생", + description: "정산 상태 변경 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setIsActionLoading(false) + } + } + + // 정산 편집 완료 핸들러 + const handleFormSubmit = (updatedSettlement) => { + setSelectedSettlement(updatedSettlement) + setModalMode("detail") + + // 목록 업데이트 + setSettlements(prevSettlements => { + return { + ...prevSettlements, + content: prevSettlements.content.map(item => + item.id === updatedSettlement.id ? updatedSettlement : item + ) + } + }) + + addToast({ + title: "정산 수정 완료", + description: "정산 내역이 성공적으로 수정되었습니다.", + }) + } + useEffect(() => { const fetchData = async () => { try { @@ -72,7 +191,6 @@ export function TeamSettlementsPage() { // 페이징 메타데이터 설정 setTotalPages(settlementResponse.totalPages) setTotalElements(settlementResponse.totalElements) - console.info(settlements) } catch (error) { console.error("팀 정산 내역 조회 오류:", error) setError(error.message) @@ -121,7 +239,10 @@ export function TeamSettlementsPage() { initialFilters={filters} teamId={teamId}/>
- + {/* 페이지네이션 컴포넌트 */}
@@ -202,6 +323,161 @@ export function TeamSettlementsPage() {
+ + {/* 모달창 */} + {isModalOpen && ( +
+
+
+ +
+ + {isModalLoading ? ( +
+

로딩 중...

+
+ ) : selectedSettlement ? ( +
+ {modalMode === "detail" ? ( + <> +
+
+
+

정산 #{String( + selectedSettlement.id).substring(0, + 8)}

+
+ +
+
+
+
+
+
+
+

결제자 + 정보

+

{selectedSettlement.payerNickname + || "알 수 없음"}

+
+
+

정산자 + 정보

+

{selectedSettlement.settlerNickname + || "알 수 없음"}

+
+
+ +
+
+

연관 + 지출

+

{selectedSettlement.expenseDescription + || "알 수 없음"}

+ {selectedSettlement.expense && ( +

+ {new Date( + selectedSettlement.expense.date).toLocaleDateString()} · + {new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW' + }).format( + selectedSettlement.expense.amount)} +

+ )} +
+ +
+

정산 + 금액

+

+ {new Intl.NumberFormat('ko-KR', { + style: 'currency', + currency: 'KRW' + }).format(selectedSettlement.amount)} +

+
+
+
+ +
+

정산 + 정보

+
+
+

생성일

+

{new Date( + selectedSettlement.createdAt).toLocaleString()}

+
+
+

수정일

+

{new Date( + selectedSettlement.updatedAt).toLocaleString()}

+
+
+
+
+
+
+ + +
+
+
+ + ) : ( +
+
+

정산 내역 수정

+
+ setModalMode("detail")} + /> +
+ )} +
+ ) : ( +
+

정산 내역을 불러올 수 + 없습니다.

+
+ )} +
+
+ )}
) } \ No newline at end of file diff --git a/Frontend/luckeyseven/src/pages/TeamDashBoard.js b/Frontend/luckeyseven/src/pages/TeamDashBoard.js index d103bba0..33edc9f3 100644 --- a/Frontend/luckeyseven/src/pages/TeamDashBoard.js +++ b/Frontend/luckeyseven/src/pages/TeamDashBoard.js @@ -1,27 +1,33 @@ -import React, { useState, useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; -import { currentTeamIdState } from '../recoil/atoms/teamAtoms'; -import { getTeamDashboard, getTeamMembers } from '../service/TeamService'; +import React, {useEffect, useState} from 'react'; +import {useRecoilValue, useSetRecoilState} from 'recoil'; +import { + currentTeamIdState, + teamForeignCurrencyState +} from '../recoil/atoms/teamAtoms'; +import {getTeamDashboard, getTeamMembers} from '../service/TeamService'; import styles from '../styles/App.module.css'; import Header from '../components/Header'; import PageHeaderControls from '../components/PageHeaderControls'; import Tabs from '../components/Tabs'; import OverviewTabContent from '../components/OverviewTabContent'; import MembersTabContent from '../components/MembersTabContent'; -import { TeamSettlementsPage } from './Settlement/TeamSettlementsPage'; +import {TeamSettlementsPage} from './Settlement/TeamSettlementsPage'; import SetBudgetDialog from '../pages/BudgetPage/components/set-budget-dialog'; -import EditBudgetDialog from '../pages/BudgetPage/components/edit-budget-dialog'; +import EditBudgetDialog + from '../pages/BudgetPage/components/edit-budget-dialog'; import AddBudgetDialog from '../pages/BudgetPage/components/add-budget-dialog'; +import ExpenseList from "./ExpenseDialog/ExpenseList"; const DoughnutChartPlaceholder = () => ( -
- Chart -
+
+ Chart +
); function TeamDashBoard() { const [activeTab, setActiveTab] = useState('Overview'); const teamId = useRecoilValue(currentTeamIdState); + const setTeamForeignCurrency = useSetRecoilState(teamForeignCurrencyState); // 외화 통화 단위 설정 함수 const [dashboardData, setDashboardData] = useState(null); const [dialogType, setDialogType] = useState(null); // 'set', 'edit', 'add', or null // 다이얼로그 인스턴스를 구분하기 위한 키 생성 상태 추가 @@ -64,10 +70,15 @@ function TeamDashBoard() { totalAmount: updatedBudget?.totalAmount || 0, balance: updatedBudget?.balance || 0, foreignBalance: updatedBudget?.foreignBalance || 0, - foreignCurrency: updatedBudget?.foreignCurrency || 'KRW', + foreignCurrency: updatedBudget?.foreignCurrency || 'USD', avgExchangeRate: updatedBudget?.avgExchangeRate || 0 } }); + + // 외화 통화 단위를 Recoil 상태로 저장 + if (updatedBudget?.foreignCurrency) { + setTeamForeignCurrency(updatedBudget.foreignCurrency); + } } setDialogType(null); @@ -87,10 +98,13 @@ function TeamDashBoard() { totalAmount: 0, balance: 0, foreignBalance: 0, - foreignCurrency: 'KRW', + foreignCurrency: 'USD', avgExchangeRate: 0 } }); + + // 외화 통화 단위 기본값 설정 + setTeamForeignCurrency('USD'); } // 모든 관련 대화상자 닫기 @@ -105,7 +119,7 @@ function TeamDashBoard() { totalAmount: 0, balance: 0, foreignBalance: 0, - foreignCurrency: 'KRW', + foreignCurrency: 'USD', avgExchangeRate: 0 } }); @@ -121,18 +135,27 @@ function TeamDashBoard() { const overviewData = await getTeamDashboard(teamId); console.log("Overview Data:", overviewData); + // 외화 통화 단위를 Recoil 상태로 저장 + if (overviewData?.foreignCurrency) { + setTeamForeignCurrency(overviewData.foreignCurrency); + } + setDashboardData({ ...overviewData, budget: { - totalAmount: budgetData?.totalAmount ?? overviewData?.totalAmount ?? 0, + totalAmount: budgetData?.totalAmount ?? overviewData?.totalAmount + ?? 0, balance: budgetData?.balance ?? overviewData?.balance ?? 0, - foreignBalance: budgetData?.foreignBalance ?? overviewData?.foreignBalance ?? 0, - foreignCurrency: budgetData?.foreignCurrency ?? overviewData?.foreignCurrency ?? 'KRW', - avgExchangeRate: budgetData?.avgExchangeRate ?? overviewData?.avgExchangeRate ?? 0, + foreignBalance: budgetData?.foreignBalance + ?? overviewData?.foreignBalance ?? 0, + foreignCurrency: budgetData?.foreignCurrency + ?? overviewData?.foreignCurrency ?? 'USD', + avgExchangeRate: budgetData?.avgExchangeRate + ?? overviewData?.avgExchangeRate ?? 0, } }); - const { teamName, teamCode, teamPassword } = overviewData || {}; + const {teamName, teamCode, teamPassword} = overviewData || {}; const teamMembers = await getTeamMembers(teamId); @@ -158,7 +181,7 @@ function TeamDashBoard() { totalAmount: 0, balance: 0, foreignBalance: 0, - foreignCurrency: 'KRW', + foreignCurrency: 'USD', expenseList: [], avgExchangeRate: 0 }; @@ -169,58 +192,61 @@ function TeamDashBoard() { }; fetchData(); - }, [teamId, budgetData, budgetInitialized]); + }, [teamId, budgetData, budgetInitialized, setTeamForeignCurrency]); return ( -
-
-
- - - {activeTab === 'Overview' && - - } - {activeTab === 'Members' && ( - - )} - {activeTab === 'Settlement' && ( - - )} - - {dialogType === 'set' && ( - - )} - {dialogType === 'edit' && ( - +
+
+ - )} - {dialogType === 'add' && ( - - )} -
-
+ + {activeTab === 'Overview' && + + } + {activeTab === 'Members' && ( + + )} + {activeTab === 'Expenses' && ( + + )} + {activeTab === 'Settlement' && ( + + )} + + {dialogType === 'set' && ( + + )} + {dialogType === 'edit' && ( + + )} + {dialogType === 'add' && ( + + )} + +
); } diff --git a/Frontend/luckeyseven/src/recoil/atoms/teamAtoms.js b/Frontend/luckeyseven/src/recoil/atoms/teamAtoms.js index 139def95..52902782 100644 --- a/Frontend/luckeyseven/src/recoil/atoms/teamAtoms.js +++ b/Frontend/luckeyseven/src/recoil/atoms/teamAtoms.js @@ -1,14 +1,17 @@ -import { atom } from 'recoil'; -import { recoilPersist } from "recoil-persist"; +import {atom} from 'recoil'; +import {recoilPersist} from "recoil-persist"; -// export const currentTeamIdState = atom({ -// key: 'currentTeamIdState', // unique ID (with respect to other atoms/selectors) -// default: 1, // default value (aka initial value) -// }); +const {persistAtom} = recoilPersist(); -const { persistAtom } = recoilPersist(); export const currentTeamIdState = atom({ key: "currentTeamIdState", default: null, // 최초엔 null, 추후 URL로부터 세팅 effects_UNSTABLE: [persistAtom], // persistAtom은 recoil-persist에서 제공하는 함수로, atom의 상태를 localStorage에 저장합니다. +}); + +// 팀의 외화 통화 단위를 저장하는 atom +export const teamForeignCurrencyState = atom({ + key: "teamForeignCurrencyState", + default: "외화", // 기본값은 USD + effects_UNSTABLE: [persistAtom], }); \ No newline at end of file