diff --git a/Backend/src/main/java/com/luckyseven/backend/domain/budget/dto/BudgetUpdateRequest.java b/Backend/src/main/java/com/luckyseven/backend/domain/budget/dto/BudgetUpdateRequest.java index e11a51b2..ef475bf9 100644 --- a/Backend/src/main/java/com/luckyseven/backend/domain/budget/dto/BudgetUpdateRequest.java +++ b/Backend/src/main/java/com/luckyseven/backend/domain/budget/dto/BudgetUpdateRequest.java @@ -1,7 +1,9 @@ package com.luckyseven.backend.domain.budget.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import com.luckyseven.backend.domain.budget.entity.CurrencyCode; import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import lombok.Getter; import lombok.experimental.SuperBuilder; diff --git a/Backend/src/main/java/com/luckyseven/backend/domain/budget/entity/Budget.java b/Backend/src/main/java/com/luckyseven/backend/domain/budget/entity/Budget.java index 14d07fdd..fb028454 100644 --- a/Backend/src/main/java/com/luckyseven/backend/domain/budget/entity/Budget.java +++ b/Backend/src/main/java/com/luckyseven/backend/domain/budget/entity/Budget.java @@ -66,13 +66,7 @@ public Budget(Team team, BigDecimal totalAmount, Long setBy, public void setTotalAmount(BigDecimal totalAmount) { this.totalAmount = totalAmount; - } - - public void addBalance(BudgetUpdateRequest request) { - if (request.additionalBudget() == null) { - return; - } - this.balance = this.balance.add(request.additionalBudget()); + this.balance = totalAmount; } public void setExchangeInfo(boolean isExchanged, BigDecimal amount, BigDecimal exchangeRate) { @@ -115,6 +109,7 @@ private void updateForeignBalance(BigDecimal amount, BigDecimal exchangeRate) { foreignBalance = BigDecimal.ZERO; } this.foreignBalance = this.foreignBalance.add(additionalBudget); + this.avgExchangeRate = exchangeRate; } public void setForeignBalance() { diff --git a/Backend/src/main/java/com/luckyseven/backend/domain/budget/service/BudgetService.java b/Backend/src/main/java/com/luckyseven/backend/domain/budget/service/BudgetService.java index 0dac2243..9a7e19f8 100644 --- a/Backend/src/main/java/com/luckyseven/backend/domain/budget/service/BudgetService.java +++ b/Backend/src/main/java/com/luckyseven/backend/domain/budget/service/BudgetService.java @@ -13,6 +13,7 @@ import com.luckyseven.backend.domain.team.repository.TeamRepository; import jakarta.persistence.EntityNotFoundException; import jakarta.transaction.Transactional; +import java.math.BigDecimal; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -34,6 +35,7 @@ public BudgetCreateResponse save(Long teamId, Long loginMemberId, BudgetCreateRe Budget budget = Budget.builder() .team(team) .totalAmount(request.totalAmount()) + .avgExchangeRate(request.exchangeRate()) .setBy(loginMemberId) .balance(request.totalAmount()) .foreignCurrency(request.foreignCurrency()) @@ -92,8 +94,8 @@ private static void addBudget(BudgetUpdateRequest request, Budget budget) { budget.updateExchangeInfo(request.isExchanged(), request.additionalBudget(), request.exchangeRate()); - budget.setTotalAmount(budget.getTotalAmount().add(request.additionalBudget())); - budget.addBalance(request); + BigDecimal sum = budget.getTotalAmount().add(request.additionalBudget()); + budget.setTotalAmount(sum); } } diff --git a/Frontend/luckeyseven/src/components/OverviewTabContent.js b/Frontend/luckeyseven/src/components/OverviewTabContent.js index 3c7ba331..7ee31153 100644 --- a/Frontend/luckeyseven/src/components/OverviewTabContent.js +++ b/Frontend/luckeyseven/src/components/OverviewTabContent.js @@ -20,10 +20,13 @@ const OverviewTabContent = ({ dashboardData }) => { avgExchangeRate = 0, // 기본값 설정 } = dashboardData; - const totalExpense = totalAmount - balance; + // totalExpense가 0 이하가 될 수 없도록 Math.max 사용 + const totalExpense = Math.max(0, totalAmount - balance); + // 남은 예산은 총 지출 - 지출로 계산하고, 항상 총 지출보다 작거나 같도록 보장 + const remainingBudget = Math.min(totalAmount, Math.max(0, totalAmount - totalExpense)); const totalExpensePercentage = totalAmount > 0 ? (totalExpense / totalAmount) * 100 : 0; - const remainingBudgetPercentage = totalAmount > 0 ? (balance / totalAmount) * 100 : 0; - + const remainingBudgetPercentage = totalAmount > 0 ? (remainingBudget / totalAmount) * 100 : 0; + // 지출 목록이 없는 경우 빈 배열로 처리 const transformedExpenses = Array.isArray(expenseList) ? expenseList.map(expense => ({ id: expense.id, @@ -40,7 +43,7 @@ const OverviewTabContent = ({ dashboardData }) => { return (
- + {foreignCurrency && foreignBalance !== undefined && foreignCurrency !== 'KRW' && ( diff --git a/Frontend/luckeyseven/src/components/SummaryCard.js b/Frontend/luckeyseven/src/components/SummaryCard.js index 47d7e9d5..bf0c1a42 100644 --- a/Frontend/luckeyseven/src/components/SummaryCard.js +++ b/Frontend/luckeyseven/src/components/SummaryCard.js @@ -13,4 +13,4 @@ const SummaryCard = ({ title, amount, currency, percentage, of }) => { ); }; -export default SummaryCard; +export default SummaryCard; \ No newline at end of file diff --git a/Frontend/luckeyseven/src/pages/BudgetPage/BudgetPage.jsx b/Frontend/luckeyseven/src/pages/BudgetPage/BudgetPage.jsx index 0e2cd200..c9a0194f 100644 --- a/Frontend/luckeyseven/src/pages/BudgetPage/BudgetPage.jsx +++ b/Frontend/luckeyseven/src/pages/BudgetPage/BudgetPage.jsx @@ -3,6 +3,7 @@ import axios from 'axios'; import { useParams } from "react-router-dom"; import SetBudgetDialog from "./components/set-budget-dialog"; import AddBudgetDialog from "./components/add-budget-dialog"; +import EditBudgetDialog from "./components/edit-budget-dialog"; import PageHeaderControls from "../../components/PageHeaderControls"; import { setBudgetInitialized } from "../../service/ApiService"; import { currentTeamIdState } from "../../recoil/atoms/teamAtoms"; @@ -129,7 +130,7 @@ export function BudgetPage() {

총 예산: {SafeFormatterUtil.formatCurrency(budget?.totalAmount)} KRW

원화 잔고: {SafeFormatterUtil.formatCurrency(budget?.balance)} KRW

외화 잔고: {SafeFormatterUtil.formatCurrency(budget?.foreignBalance)} {budget?.foreignCurrency || 'KRW'}

-

평균 환율: {budget?.avgExchangeRate || 0}

+

평균 환율: {SafeFormatterUtil.formatCurrency(budget?.avgExchangeRate)}

) : (
@@ -152,6 +153,14 @@ export function BudgetPage() { /> )} + {dialogType === "edit" && ( + + )} + {dialogType === "add" && ( { +const AddBudgetDialog = ({ teamId, budgetId, closeDialog, onBudgetUpdate }) => { const [additionalBudget, setAdditionalBudget] = useState(0); const [isExchanged, setIsExchanged] = useState(false); const [exchangeRate, setExchangeRate] = useState(''); + const [foreignCurrency, setForeignCurrency] = useState('KRW'); const [isSubmitting, setIsSubmitting] = useState(false); + const [initialLoaded, setInitialLoaded] = useState(false); + const [error, setError] = useState(''); + const [currentBudgetData, setCurrentBudgetData] = useState(null); + + useEffect(() => { + const fetchBudget = async () => { + try { + // budgetId가 없을 경우, team의 예산을 fetch하려 시도 + const url = budgetId + ? `/api/teams/${teamId}/budget/${budgetId}` + : `/api/teams/${teamId}/budget`; + + const response = await axios.get(url); + const budget = response.data; + + setCurrentBudgetData(budget); // 기존 예산 데이터 저장 + setAdditionalBudget(0); + setIsExchanged(!!budget.avgExchangeRate); + setForeignCurrency(budget.foreignCurrency || 'KRW'); + setExchangeRate(''); + setInitialLoaded(true); + } catch (error) { + console.error('Error fetching budget:', error); + // defaults data + setAdditionalBudget(0); + setIsExchanged(false); + setExchangeRate(''); + setInitialLoaded(true); + + if (error.response && error.response.status === 404) { + setError('예산 정보를 찾을 수 없습니다. 먼저 예산을 설정해주세요.'); + } else { + setError('예산 정보를 불러오는 중 오류가 발생했습니다.'); + } + } + }; + + fetchBudget(); + }, [teamId, budgetId]); const resetForm = () => { setAdditionalBudget(0); setIsExchanged(false); setExchangeRate(''); + setError(''); }; const handleClose = () => { @@ -22,14 +63,29 @@ const AddBudgetDialog = ({ teamId, closeDialog, onBudgetUpdate }) => { const handleSubmit = async () => { if (isSubmitting) return; setIsSubmitting(true); + setError(''); + + // 입력값 유효성 검사 + if (additionalBudget <= 0) { + setError('금액은 0보다 커야 합니다.'); + setIsSubmitting(false); + return; + } + + if (isExchanged && (!exchangeRate || exchangeRate <= 0)) { + setError('유효한 환율을 입력해주세요.'); + setIsSubmitting(false); + return; + } try { const response = await axios.patch(`/api/teams/${teamId}/budget`, { - additionalBudget, + additionalBudget: Number(additionalBudget), isExchanged, - exchangeRate: isExchanged ? exchangeRate : null, + exchangeRate: isExchanged ? Number(exchangeRate) : null, }); - console.log(response.data); + + console.log('Budget update response:', response.data); if (onBudgetUpdate) { onBudgetUpdate(response.data); @@ -38,17 +94,54 @@ const AddBudgetDialog = ({ teamId, closeDialog, onBudgetUpdate }) => { resetForm(); closeDialog(); } catch (error) { - console.error('Error adding budget:', error); - alert('예산 추가 중 오류가 발생했습니다: ' + (error.response?.data?.message || error.message)); + console.error('Error updating budget:', error); + + if (error.response) { + if (error.response.status === 404) { + setError('예산 정보를 찾을 수 없습니다. 먼저 예산을 설정해주세요.'); + } else { + setError('예산 수정 중 오류가 발생했습니다: ' + (error.response.data?.message || error.message)); + } + } else { + setError('서버와 통신 중 오류가 발생했습니다.'); + } } finally { setIsSubmitting(false); } }; + if (!initialLoaded) { + return ( +
+
+

예산 정보 로딩 중...

+
+
+ ); + } + + // 예산 정보를 불러올 수 없는 경우 에러 메시지 표시 + if (error && !currentBudgetData) { + return ( +
+
e.stopPropagation()}> +

예산 수정

+
{error}
+
+ +
+
+
+ ); + } + return (
e.stopPropagation()}>

예산 추가

+ + {error &&
{error}
} + { const [totalAmount, setTotalAmount] = useState(0); const [isExchanged, setIsExchanged] = useState(false); + const [foreignCurrency, setForeignCurrency] = useState('KRW'); const [exchangeRate, setExchangeRate] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [initialLoaded, setInitialLoaded] = useState(false); + const [error, setError] = useState(''); + const [currentBudgetData, setCurrentBudgetData] = useState(null); useEffect(() => { const fetchBudget = async () => { try { - // If budgetId is not provided, try to fetch the team's budget + // budgetId가 없을 경우, team의 예산을 fetch하려 시도 const url = budgetId ? `/api/teams/${teamId}/budget/${budgetId}` : `/api/teams/${teamId}/budget`; const response = await axios.get(url); const budget = response.data; - setTotalAmount(budget.balance || budget.totalAmount || 0); - setIsExchanged(!!budget.isExchanged); + + setCurrentBudgetData(budget); // 기존 예산 데이터 저장 + setTotalAmount(budget.totalAmount || 0); + setIsExchanged(!!budget.avgExchangeRate); + setForeignCurrency(budget.foreignCurrency || 'KRW'); setExchangeRate(budget.avgExchangeRate || ''); setInitialLoaded(true); } catch (error) { console.error('Error fetching budget:', error); - // If we can't fetch the budget data, set defaults + setTotalAmount(0); setIsExchanged(false); + setForeignCurrency('KRW'); setExchangeRate(''); setInitialLoaded(true); + + if (error.response && error.response.status === 404) { + setError('예산 정보를 찾을 수 없습니다. 먼저 예산을 설정해주세요.'); + } else { + setError('예산 정보를 불러오는 중 오류가 발생했습니다.'); + } } }; @@ -40,6 +53,7 @@ const EditBudgetDialog = ({ teamId, budgetId, closeDialog, onBudgetUpdate }) => setTotalAmount(0); setIsExchanged(false); setExchangeRate(''); + setError(''); }; const handleClose = () => { @@ -50,14 +64,30 @@ const EditBudgetDialog = ({ teamId, budgetId, closeDialog, onBudgetUpdate }) => const handleSubmit = async () => { if (isSubmitting) return; setIsSubmitting(true); + setError(''); + + // 입력값 유효성 검사 + if (totalAmount <= 0) { + setError('예산 금액은 0보다 커야 합니다.'); + setIsSubmitting(false); + return; + } + + if (isExchanged && (!exchangeRate || exchangeRate <= 0)) { + setError('유효한 환율을 입력해주세요.'); + setIsSubmitting(false); + return; + } try { const response = await axios.patch(`/api/teams/${teamId}/budget`, { - totalAmount, + totalAmount: Number(totalAmount), isExchanged, - exchangeRate: isExchanged ? exchangeRate : null, + foreignCurrency, + exchangeRate: isExchanged ? Number(exchangeRate) : null, }); - console.log(response.data); + + console.log('Budget update response:', response.data); if (onBudgetUpdate) { onBudgetUpdate(response.data); @@ -67,13 +97,21 @@ const EditBudgetDialog = ({ teamId, budgetId, closeDialog, onBudgetUpdate }) => closeDialog(); } catch (error) { console.error('Error updating budget:', error); - alert('예산 수정 중 오류가 발생했습니다: ' + (error.response?.data?.message || error.message)); + + if (error.response) { + if (error.response.status === 404) { + setError('예산 정보를 찾을 수 없습니다. 먼저 예산을 설정해주세요.'); + } else { + setError('예산 수정 중 오류가 발생했습니다: ' + (error.response.data?.message || error.message)); + } + } else { + setError('서버와 통신 중 오류가 발생했습니다.'); + } } finally { setIsSubmitting(false); } }; - // If we're still loading the initial data, show a loading indicator if (!initialLoaded) { return (
@@ -84,20 +122,61 @@ const EditBudgetDialog = ({ teamId, budgetId, closeDialog, onBudgetUpdate }) => ); } + // 예산 정보를 불러올 수 없는 경우 에러 메시지 표시 + if (error && !currentBudgetData) { + return ( +
+
e.stopPropagation()}> +

예산 수정

+
{error}
+
+ +
+
+
+ ); + } + return (
e.stopPropagation()}>

예산 수정

+ + {error &&
{error}
} + setTotalAmount(e.target.value)} placeholder="수정할 예산 금액" - min = "0" - step = "100" + min="0" + step="100" /> + + +