diff --git a/index.html b/index.html index 4d1b104..dc856dc 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + 나만의 회계 비서, PayCheck diff --git a/public/icon.svg b/public/icon.svg index 18e25f3..a60e494 100644 --- a/public/icon.svg +++ b/public/icon.svg @@ -1,9 +1,9 @@ - - - - - + + + + + diff --git a/src/components/KakaoShareBtn.tsx b/src/components/KakaoShareBtn.tsx new file mode 100644 index 0000000..de6fd16 --- /dev/null +++ b/src/components/KakaoShareBtn.tsx @@ -0,0 +1,72 @@ +import { useEffect } from 'react'; + +declare global { + interface Window { + Kakao: any; + } +} + +interface KakaoShareButtonProps { + title: string; + description: string; + imageUrl: string; + linkUrl: string; +} + +export default function KakaoShareBtn({ + title, + description, + imageUrl, + linkUrl, +}: KakaoShareButtonProps) { + useEffect(() => { + // Kakao SDK 스크립트 추가 + if (!window.Kakao && !document.getElementById('kakao-sdk')) { + const script = document.createElement('script'); + script.id = 'kakao-sdk'; + script.src = 'https://developers.kakao.com/sdk/js/kakao.js'; + script.onload = () => { + window.Kakao.init('c4913a27ee144670505405de9ee16631'); // 카카오 JavaScript 키 + console.log('Kakao SDK initialized:', window.Kakao.isInitialized()); + }; + document.head.appendChild(script); + } else if (window.Kakao && !window.Kakao.isInitialized()) { + window.Kakao.init('c4913a27ee144670505405de9ee16631'); // 이미 로드된 경우 초기화만 + } + }, []); + + const shareToKakao = () => { + if (!window.Kakao) return; + + window.Kakao.Link.sendDefault({ + objectType: 'feed', + content: { + title, + description, + imageUrl, + link: { + mobileWebUrl: linkUrl, + webUrl: linkUrl, + }, + }, + buttons: [ + { + title: '웹으로 보기', + link: { + mobileWebUrl: linkUrl, + webUrl: linkUrl, + }, + }, + ], + }); + }; + + return ( + + ); +} diff --git a/src/components/ReceiptDetail.tsx b/src/components/ReceiptDetail.tsx new file mode 100644 index 0000000..c52a291 --- /dev/null +++ b/src/components/ReceiptDetail.tsx @@ -0,0 +1,172 @@ +import React, { useState, useEffect } from 'react'; +import type { Receipt } from '../types/receipt'; // types/receipt 파일 경로 확인 필요 + +interface ReceiptDetailProps { + receiptData: Receipt[]; // 영수증 품목 데이터 배열 + allowedParticipants: string[]; // 추가: 허용된 참여자 명단 + settleType: 'even' | 'item'; // 추가: 정산 방식 +} + +const ReceiptDetail: React.FC = ({ receiptData, allowedParticipants, settleType }) => { + // 영수증 데이터가 없거나 비어있으면 처리 + if (!receiptData || receiptData.length === 0) { + return
영수증 내역이 없습니다.
; + } + + // 상호명은 첫 번째 품목 데이터에서 가져옴 + const storeName = receiptData[0].store_name; + + // 각 품목별 참여자를 관리하기 위한 상태 (품목 index -> 참여자 이름 배열) + const [itemParticipants, setItemParticipants] = useState( + receiptData.map(() => settleType === 'even' ? allowedParticipants : []) + ); + + // settleType 또는 allowedParticipants가 변경될 때 itemParticipants 상태를 업데이트 + useEffect(() => { + if (settleType === 'even') { + setItemParticipants(receiptData.map(() => allowedParticipants)); + } else { + setItemParticipants(receiptData.map(() => [])); + } + }, [settleType, allowedParticipants, receiptData]); // 의존성 배열에 settleType, allowedParticipants, receiptData 추가 + + // 각 품목별 오류 메시지를 관리하기 위한 상태 (품목 index -> 오류 메시지 문자열) + const [itemErrors, setItemErrors] = useState( + receiptData.map(() => '') + ); + + // 특정 품목에 참여자 태그 추가 핸들러 + const handleAddParticipant = (itemIndex: number, participantName: string) => { + const trimmedName = participantName.trim(); + + // 입력이 비어있으면 오류 메시지 제거 후 종료 + if (trimmedName === '') { + setItemErrors(prevErrors => { + const newErrors = [...prevErrors]; + newErrors[itemIndex] = ''; + return newErrors; + }); + return; + } + + // 허용된 참여자 명단에 있는지 확인 + if (!allowedParticipants.includes(trimmedName)) { + // 명단에 없으면 오류 메시지 설정 + setItemErrors(prevErrors => { + const newErrors = [...prevErrors]; + newErrors[itemIndex] = '리스트에 있는 이름이 아닙니다'; + return newErrors; + }); + return; // 참여자 추가 방지 + } + + // 명단에 있으면 오류 메시지 제거 및 참여자 추가 + setItemErrors(prevErrors => { + const newErrors = [...prevErrors]; + newErrors[itemIndex] = ''; // 성공 시 오류 메시지 초기화 + return newErrors; + }); + + setItemParticipants(prevParticipants => { + const newParticipants = [...prevParticipants]; + // 해당 품목의 참여자 목록에 추가 (중복 방지) + if (!newParticipants[itemIndex].includes(trimmedName)) { + newParticipants[itemIndex] = [...newParticipants[itemIndex], trimmedName]; + } + return newParticipants; + }); + }; + + // 특정 품목의 참여자 태그 삭제 핸들러 + const handleRemoveParticipant = (itemIndex: number, participantToRemove: string) => { + setItemParticipants(prevParticipants => { + const newParticipants = [...prevParticipants]; + newParticipants[itemIndex] = newParticipants[itemIndex].filter( + participant => participant !== participantToRemove + ); + return newParticipants; + }); + }; + + // 입력 필드 내용 변경 시 해당 품목의 오류 메시지 제거 + const handleInputChange = (itemIndex: number) => { + setItemErrors(prevErrors => { + const newErrors = [...prevErrors]; + newErrors[itemIndex] = ''; + return newErrors; + }); + }; + + return ( +
{/* 전체 컨테이너 너비 꽉 채우기 */} + {/* 상호명 및 날짜 */} +

{storeName}

+ + {/* 영수증 품목 테이블 */} + {/* 테이블 너비 꽉 채우기, 테두리 병합 */} + {/* 헤더 텍스트 좌측 정렬 */} + + {/* 품목 열 너비 설정 */} + {/* 수량 열 너비 설정 */} + {/* 금액 열 너비 설정 */} + + + + + {receiptData.map((item, index) => ( + + + + + + + + {itemParticipants[index].length > 0 && ( + + + + )} + + ))} + +
품목수량금액참여자
{item.item_name}{item.quantity}{item.total_amount} + {/* 오류 메시지 표시 */} + {itemErrors[index] && ( +

{itemErrors[index]}

+ )} + { + if (e.key === 'Enter') { + const inputElement = e.target as HTMLInputElement; + handleAddParticipant(index, inputElement.value); + // 참여자 추가 후 입력 필드 초기화 + inputElement.value = ''; + } + }} + onChange={() => handleInputChange(index)} // 입력 변경 시 오류 메시지 제거 + /> +
+
+ {itemParticipants[index].map((participant, pIndex) => ( + + {participant} + + + ))} +
+
+
+ ); +}; + +export default ReceiptDetail; \ No newline at end of file diff --git a/src/layouts/Layout.tsx b/src/layouts/Layout.tsx index d216d3d..7639abf 100644 --- a/src/layouts/Layout.tsx +++ b/src/layouts/Layout.tsx @@ -20,15 +20,6 @@ const Layout = ({ children }: LayoutProps) => { />
PayCheck
- -
- - -
{/* 메인 컨텐츠 */}
{children}
diff --git a/src/pages/CheckPage.tsx b/src/pages/CheckPage.tsx index 87f7509..ad3d1d0 100644 --- a/src/pages/CheckPage.tsx +++ b/src/pages/CheckPage.tsx @@ -1,40 +1,140 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import ReceiptDetail from '../components/ReceiptDetail'; +import { axiosInstance } from '../apis/axios'; +import type { Receipt } from '../types/receipt'; +import { useLocation } from 'react-router-dom'; +import KakaoShareBtn from '../components/KakaoShareBtn'; + const CheckPage = () => { + const [rawReceiptItems, setRawReceiptItems] = useState([]); + const [groupedReceipts, setGroupedReceipts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [totalAmount, setTotalAmount] = useState(0); + const [allowedParticipants, setAllowedParticipants] = useState([]); + + const navigate = useNavigate(); + const location = useLocation(); + const { settleType } = location.state as { settleType: 'even' | 'item' } || { settleType: 'even' }; // state에서 settleType 가져오기 (기본값 'even') + + useEffect(() => { + const fetchReceipts = async () => { + try { + const response = await axiosInstance.get<{ success: boolean; results: Receipt[] }>('api/receiptinfo/analyze/'); + + const items = response.data.results; + + setRawReceiptItems(items); + console.log('API 응답 데이터:', response.data); + + const groupedData = groupReceiptItemsByReceiptId(items); + setGroupedReceipts(groupedData); + + const calculatedTotal = items.reduce((sum, item) => sum + item.total_amount, 0); + setTotalAmount(calculatedTotal); + + const fetchParticipants = async () => { + try { + const participantResponse = await axiosInstance.get<{ success: boolean; message: string; data: { id: number; name: string }[] }>('api/participant/members/'); + console.log('GET 요청 응답 데이터:', participantResponse.data); + if (participantResponse.data && Array.isArray(participantResponse.data.data)) { + setAllowedParticipants(participantResponse.data.data.map(member => member.name)); + } else { + console.error('참여자 명단 데이터 형식이 예상과 다릅니다.', participantResponse.data); + setAllowedParticipants([]); + } + } catch (participantError: any) { + console.error('GET 요청 실패:', participantError); + } + }; + + fetchParticipants(); + + } catch (err: any) { + console.error('영수증 데이터 불러오기 실패:', err); + setError('영수증 데이터를 불러오는데 실패했습니다.'); + } finally { + setLoading(false); + } + }; + + fetchReceipts(); + }, []); + + const groupReceiptItemsByReceiptId = (items: Receipt[]): Receipt[][] => { + const receiptsMap = new Map(); + + items.forEach(item => { + if (!receiptsMap.has(item.receipt)) { + receiptsMap.set(item.receipt, []); + } + receiptsMap.get(item.receipt)!.push(item); + }); + + return Array.from(receiptsMap.values()); + }; + + if (loading) { + return
로딩 중...
; + } + + if (error) { + return
오류: {error}
; + } + + if (groupedReceipts.length === 0 && rawReceiptItems.length === 0) { + return
결제 내역이 없습니다.
; + } + + if (groupedReceipts.length === 0 && rawReceiptItems.length > 0) { + return
데이터는 있지만, 처리 중 문제가 발생했습니다.
; + } + return ( -
-
결제내역
-
25/05/08 동국대학교생활협동조합
-
-
-
품목
-
수량
-
금액
-
참여자
-
-
-
삼겹김치철판
-
2
-
13000
-
모수진
-
-
-
-
총액
-
13000원
-
-
-
정산 결과
-
-
하승연
-
모수진
-
-
-
-
엑셀로 내보내기
-
공유하기
+
+
+
+

결제 내역

+ +
+ {groupedReceipts.map((receiptItems, index) => ( + + ))} +
+ +

총액 {totalAmount}원

+
+ +
+

정산 결과

+ +
+ +
+
+ + + +
+ + +
+
-
-
- ) + ); }; export default CheckPage; \ No newline at end of file diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index 1a07c43..3f2fa8b 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -9,10 +9,10 @@ const LandingPage = () => { return (
-
- 나만의 회계 비서, +
+ 나만의 회계 비서, - logo + logo