diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..1552e97d --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push:*)" + ] + } +} diff --git a/.nvmrc b/.nvmrc index ec09f38d..829e9737 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.17.0 \ No newline at end of file +20.19.0 \ No newline at end of file diff --git a/src/entities/group/api/group.ts b/src/entities/group/api/group.ts index a48ba2c9..dc8ffce1 100644 --- a/src/entities/group/api/group.ts +++ b/src/entities/group/api/group.ts @@ -4,6 +4,9 @@ import { CreateGroupData, Group, GroupHeaderResponse, + SettlementGroup, + SettlementSort, + SettlementStatus, } from '@/entities/group/model/group.type'; export const getGroupList = async (): Promise => { @@ -47,3 +50,13 @@ export const getGroupHeader = ( .get(`/group/header?groupToken=${groupToken}`) .then((res) => res.data); }; + +export const getSettlementList = ( + status: SettlementStatus, + sort: SettlementSort, + limit = 100 +): Promise => { + return axiosInstance + .get('/groups', { params: { status, sort, limit } }) + .then((res) => res.data); +}; diff --git a/src/entities/group/model/group.type.ts b/src/entities/group/model/group.type.ts index 6443f59b..dc9023fc 100644 --- a/src/entities/group/model/group.type.ts +++ b/src/entities/group/model/group.type.ts @@ -28,3 +28,17 @@ export interface GroupHeaderResponse { bank: string; accountNumber: string; } + +export type SettlementStatus = 'ALL' | 'IN_PROGRESS' | 'COMPLETED'; +export type SettlementSort = 'LATEST' | 'OLDEST'; + +export interface SettlementGroup { + groupId: number; + groupCode: string; + name: string; + totalAmount: number; + totalMemberCount: number; + completedMemberCount: number; + createdAt: string; + completedAt: string | null; +} diff --git a/src/entities/payment/api/payment.ts b/src/entities/payment/api/payment.ts index a137f2f2..9cc11dbf 100644 --- a/src/entities/payment/api/payment.ts +++ b/src/entities/payment/api/payment.ts @@ -1,11 +1,22 @@ import axiosInstance from '@/shared/api/axios'; -import { PaymentList } from '@/entities/payment/model/payment.type'; +import { + PaymentActionResult, + PaymentList, +} from '@/entities/payment/model/payment.type'; const payment = { getAll: (): Promise => - // TODO: 모의 데이터 제거 후 실제 API 연동 시 삭제 useMock: true 옵션 제거 - // axiosInstance.get('/payments', { useMock: true }).then((res) => res.data), axiosInstance.get('/payments').then((res) => res.data), + + approve: (paymentRequestId: number): Promise => + axiosInstance + .patch(`/payments/${paymentRequestId}/approve`) + .then((res) => res.data), + + reject: (paymentRequestId: number): Promise => + axiosInstance + .patch(`/payments/${paymentRequestId}/reject`) + .then((res) => res.data), }; export default payment; diff --git a/src/entities/payment/model/payment.type.ts b/src/entities/payment/model/payment.type.ts index b96f7b0d..96f85c35 100644 --- a/src/entities/payment/model/payment.type.ts +++ b/src/entities/payment/model/payment.type.ts @@ -10,3 +10,15 @@ export interface PaymentRequest { export interface PaymentList { paymentRequests: PaymentRequest[]; } + +export type PaymentStatus = 'APPROVED' | 'REJECTED'; + +export interface PaymentActionResult { + id: number; + settlementId: number; + requestMemberId: number; + targetUserId: number; + requestedAt: string; + processedAt: string; + status: PaymentStatus; +} diff --git a/src/features/home/api/useGetSettlementList.ts b/src/features/home/api/useGetSettlementList.ts new file mode 100644 index 00000000..f6f112c9 --- /dev/null +++ b/src/features/home/api/useGetSettlementList.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; +import { getSettlementList } from '@/entities/group/api/group'; +import type { + SettlementSort, + SettlementStatus, +} from '@/entities/group/model/group.type'; + +const useGetSettlementList = ( + status: SettlementStatus, + sort: SettlementSort +) => { + return useQuery({ + queryKey: ['settlementList', status, sort], + queryFn: () => getSettlementList(status, sort), + }); +}; + +export default useGetSettlementList; diff --git a/src/features/payment-management/api/useApprovePayment.ts b/src/features/payment-management/api/useApprovePayment.ts new file mode 100644 index 00000000..5c8c98eb --- /dev/null +++ b/src/features/payment-management/api/useApprovePayment.ts @@ -0,0 +1,14 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import payment from '@/entities/payment/api/payment'; + +const useApprovePayment = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (paymentRequestId: number) => payment.approve(paymentRequestId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['payments'] }); + }, + }); +}; + +export default useApprovePayment; diff --git a/src/features/payment-management/api/useRejectPayment.ts b/src/features/payment-management/api/useRejectPayment.ts new file mode 100644 index 00000000..3b2d91c5 --- /dev/null +++ b/src/features/payment-management/api/useRejectPayment.ts @@ -0,0 +1,14 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import payment from '@/entities/payment/api/payment'; + +const useRejectPayment = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (paymentRequestId: number) => payment.reject(paymentRequestId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['payments'] }); + }, + }); +}; + +export default useRejectPayment; diff --git a/src/pages/home/ui/HomePageSection/index.tsx b/src/pages/home/ui/HomePageSection/index.tsx index 3f06eb87..581122db 100644 --- a/src/pages/home/ui/HomePageSection/index.tsx +++ b/src/pages/home/ui/HomePageSection/index.tsx @@ -9,6 +9,13 @@ import { useState } from 'react'; import CoinImg from '@/shared/assets/pngs/CoinImg.png'; import LinkMain from '@/shared/assets/pngs/link_main.png'; import CardMain from '@/shared/assets/pngs/card_main.png'; +import { format } from 'date-fns'; +import { ko } from 'date-fns/locale/ko'; +import useGetSettlementList from '@/features/home/api/useGetSettlementList'; +import type { + SettlementSort, + SettlementStatus, +} from '@/entities/group/model/group.type'; import Flex from '@/shared/ui/Flex'; import Button from '@/shared/ui/Button'; @@ -17,38 +24,7 @@ import Chip from '@/shared/ui/Chip'; import * as S from './index.style'; import HomeExpenseItem from '../HomeExpenseItem'; -type SettlementType = 'IN_PROGRESS' | 'COMPLETED'; - -interface HomeExpenseItemType { - date: string; - groupName: string; - totalAmount: number; - paidMember: number; - totalMember: number; - id: number; -} -/** - * @Todo 진행중인 정산 내역 조회 API 함수 호출 - * 우선 mock data로 대체 - * */ -const settlementListMock: HomeExpenseItemType[] = [ - { - id: 1, - date: '2026년 2월 22일', - groupName: 'DND 데모데이', - totalAmount: 120000, - paidMember: 3, - totalMember: 6, - }, - { - id: 2, - date: '2026년 1월 14일', - groupName: 'DND 7조 첫모임', - totalAmount: 150000, - paidMember: 5, - totalMember: 6, - }, -]; +type SettlementType = SettlementStatus; export function MainHeader() { const theme = useTheme(); @@ -128,10 +104,92 @@ export function SettlementBanner() { ); } +type SettlementContentProps = { + isLoading: boolean; + isError: boolean; + settlementList: NonNullable['data']>; + settlementType: SettlementType; +}; + +function SettlementContent({ + isLoading, + isError, + settlementList, + settlementType, +}: SettlementContentProps) { + if (isLoading) { + return ( + + + 정산 내역을 불러오는 중이에요. + + + ); + } + if (isError) { + return ( + + + 정산 내역을 불러오지 못했어요. + + + ); + } + if (settlementList.length > 0) { + return ( + + {settlementList.map((item) => ( + + ))} + + ); + } + return ( + + + + {settlementType === 'IN_PROGRESS' + ? '아직 진행중인 정산이 없어요.' + : '완료된 정산이 없어요.'} + + + ); +} + export function SettlementList() { const [settlementType, setSettlementType] = useState('IN_PROGRESS'); - const [sortToggle, setSortToggle] = useState(false); + const [sort, setSort] = useState('LATEST'); const theme = useTheme(); const handleSettlementTypeButtonClick = (type: SettlementType) => { @@ -142,12 +200,14 @@ export function SettlementList() { }; const handleSortOptionClick = () => { - setSortToggle(!sortToggle); + setSort((prev) => (prev === 'LATEST' ? 'OLDEST' : 'LATEST')); }; - const settlementList = sortToggle - ? [...settlementListMock].reverse() - : settlementListMock; + const { data, isLoading, isError } = useGetSettlementList( + settlementType, + sort + ); + const settlementList = data ?? []; return ( @@ -175,47 +235,24 @@ export function SettlementList() { {/** @Todo Select 컴포넌트 개발 후 변경 */} - {settlementList.length > 0 && settlementType === 'IN_PROGRESS' && ( - - {settlementList.map((data) => ( - - ))} - - )} - {settlementType === 'COMPLETED' && ( - - - - 아직 진행중인 정산이 없어요. - - - )} + ); } diff --git a/src/pages/paymentManagement/PaymentManagementPage.tsx b/src/pages/paymentManagement/PaymentManagementPage.tsx index d7e677be..416ce877 100644 --- a/src/pages/paymentManagement/PaymentManagementPage.tsx +++ b/src/pages/paymentManagement/PaymentManagementPage.tsx @@ -1,6 +1,8 @@ import { useMemo } from 'react'; import { ArrowLeft } from '@/shared/assets/svgs/icon'; import useGetPayments from '@/features/payment-management/api/useGetPayments'; +import useApprovePayment from '@/features/payment-management/api/useApprovePayment'; +import useRejectPayment from '@/features/payment-management/api/useRejectPayment'; import { groupPaymentRequestsByDate } from '@/features/payment-management/lib/groupPaymentRequestsBySection'; import PaymentAlert from '@/features/payment-management/ui/PaymentAlert'; import Header from '@/shared/ui/Header'; @@ -17,6 +19,8 @@ function PaymentManagementPage() { const { color } = useTheme(); const { data, isLoading, isError } = useGetPayments(); + const { mutate: approvePayment } = useApprovePayment(); + const { mutate: rejectPayment } = useRejectPayment(); const paymentSections = useMemo( () => @@ -76,12 +80,12 @@ function PaymentManagementPage() { ); } - const handleReject = (_payment: PaymentRequest) => { - // TODO: 입금 거절 API 연동 + const handleReject = (payment: PaymentRequest) => { + rejectPayment(payment.paymentRequestId); }; - const handleConfirm = (_payment: PaymentRequest) => { - // TODO: 입금 확인 API 연동 + const handleConfirm = (payment: PaymentRequest) => { + approvePayment(payment.paymentRequestId); }; return (