Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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())
Expand Down Expand Up @@ -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);
}
}

Expand Down
11 changes: 7 additions & 4 deletions Frontend/luckeyseven/src/components/OverviewTabContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -40,7 +43,7 @@ const OverviewTabContent = ({ dashboardData }) => {
return (
<div>
<div className={styles.summaryCardContainer}>
<SummaryCard title="총 예산" amount={totalAmount} currency="₩"/>
<SummaryCard title="총 예산" amount={totalAmount} currency="₩" />
<SummaryCard title="총 지출" amount={totalExpense} currency="₩" percentage={totalExpensePercentage.toFixed(1)} of="of budget" />
<SummaryCard title="남은 예산" amount={balance} currency="₩" percentage={remainingBudgetPercentage.toFixed(1)} of="of budget" />
{foreignCurrency && foreignBalance !== undefined && foreignCurrency !== 'KRW' && (
Expand Down
2 changes: 1 addition & 1 deletion Frontend/luckeyseven/src/components/SummaryCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ const SummaryCard = ({ title, amount, currency, percentage, of }) => {
);
};

export default SummaryCard;
export default SummaryCard;
11 changes: 10 additions & 1 deletion Frontend/luckeyseven/src/pages/BudgetPage/BudgetPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -129,7 +130,7 @@ export function BudgetPage() {
<p>총 예산: <span className="font-medium">{SafeFormatterUtil.formatCurrency(budget?.totalAmount)} KRW</span></p>
<p>원화 잔고: <span className="font-medium">{SafeFormatterUtil.formatCurrency(budget?.balance)} KRW</span></p>
<p>외화 잔고: <span className="font-medium">{SafeFormatterUtil.formatCurrency(budget?.foreignBalance)} {budget?.foreignCurrency || 'KRW'}</span></p>
<p>평균 환율: <span className="font-medium">{budget?.avgExchangeRate || 0}</span></p>
<p>평균 환율: <span className="font-medium">{SafeFormatterUtil.formatCurrency(budget?.avgExchangeRate)}</span></p>
</div>
) : (
<div className="text-center p-4 bg-gray-100 rounded-lg">
Expand All @@ -152,6 +153,14 @@ export function BudgetPage() {
/>
)}

{dialogType === "edit" && (
<EditBudgetDialog
teamId={teamId}
closeDialog={handleClose}
onBudgetUpdate={handleBudgetUpdate}
/>
)}

{dialogType === "add" && (
<AddBudgetDialog
teamId={teamId}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,58 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import '../styles/BudgetDialog.css';

const AddBudgetDialog = ({ teamId, closeDialog, onBudgetUpdate }) => {
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 = () => {
Expand All @@ -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);
Expand All @@ -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 (
<div className="modal-overlay">
<div className="modal">
<h2>예산 정보 로딩 중...</h2>
</div>
</div>
);
}

// 예산 정보를 불러올 수 없는 경우 에러 메시지 표시
if (error && !currentBudgetData) {
return (
<div className="modal-overlay" onClick={handleClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>예산 수정</h2>
<div className="error-message">{error}</div>
<div className="modal-buttons">
<button onClick={handleClose}>닫기</button>
</div>
</div>
</div>
);
}

return (
<div className="modal-overlay" onClick={handleClose}>
<div className="modal" onClick={(e) => e.stopPropagation()}>
<h2>예산 추가</h2>

{error && <div className="error-message">{error}</div>}

<label>추가 예산 금액</label>
<input
type="number"
Expand Down
Loading