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
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon3.svg" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>나만의 회계 비서, PayCheck</title>
</head>
Expand Down
10 changes: 5 additions & 5 deletions public/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions src/components/KakaoShareBtn.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<button
className="w-[190px] h-[57px] bg-[#0083FF] hover:bg-[#0069CD] duration-200 rounded-[18px] cursor-pointer"
onClick={shareToKakao}
>
공유하기
</button>
);
}
172 changes: 172 additions & 0 deletions src/components/ReceiptDetail.tsx
Original file line number Diff line number Diff line change
@@ -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<ReceiptDetailProps> = ({ receiptData, allowedParticipants, settleType }) => {
// 영수증 데이터가 없거나 비어있으면 처리
if (!receiptData || receiptData.length === 0) {
return <div>영수증 내역이 없습니다.</div>;
}

// 상호명은 첫 번째 품목 데이터에서 가져옴
const storeName = receiptData[0].store_name;

// 각 품목별 참여자를 관리하기 위한 상태 (품목 index -> 참여자 이름 배열)
const [itemParticipants, setItemParticipants] = useState<string[][]>(
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<string[]>(
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 (
<div className="receipt-detail-container w-full cursor-default"> {/* 전체 컨테이너 너비 꽉 채우기 */}
{/* 상호명 및 날짜 */}
<h2 className="text-[28px] font-bold font-['Inter'] text-[#51BE5A] mb-4">{storeName}</h2>

{/* 영수증 품목 테이블 */}
<table className="receipt-items-table w-full border-collapse"> {/* 테이블 너비 꽉 채우기, 테두리 병합 */}
<thead className="text-left"> {/* 헤더 텍스트 좌측 정렬 */}
<tr className="text-[24px] font-bold font-['Inter'] text-[#525761]">
<th className="w-2/5 px-2 pb-2">품목</th> {/* 품목 열 너비 설정 */}
<th className="w-1/5 px-2 pb-2">수량</th> {/* 수량 열 너비 설정 */}
<th className="w-1/5 px-2 pb-2">금액</th> {/* 금액 열 너비 설정 */}
<th className="w-1/5 px-2 pb-2 text-center">참여자</th>
</tr>
</thead>
<tbody>
{receiptData.map((item, index) => (
<React.Fragment key={index}>
<tr className="text-[21px] font-bold font-['Inter'] text-[#525761]">
<td className="px-2 py-3">{item.item_name}</td>
<td className="px-2 py-3">{item.quantity}</td>
<td className="px-2 py-3">{item.total_amount}</td>
<td className="px-2 py-3 relative">
{/* 오류 메시지 표시 */}
{itemErrors[index] && (
<p className="text-red-500 text-sm absolute bottom-13 left-0 w-full text-left px-3">{itemErrors[index]}</p>
)}
<input
type="text"
placeholder="참여자 추가"
className="w-[200px] h-[40px] rounded-[12px] text-[16px] font-medium outline-none bg-[#F5F5F5] px-3"
onKeyDown={(e) => {
if (e.key === 'Enter') {
const inputElement = e.target as HTMLInputElement;
handleAddParticipant(index, inputElement.value);
// 참여자 추가 후 입력 필드 초기화
inputElement.value = '';
}
}}
onChange={() => handleInputChange(index)} // 입력 변경 시 오류 메시지 제거
/>
</td>
</tr>
{itemParticipants[index].length > 0 && (
<tr key={`tags-${index}`}>
<td colSpan={4} className="px-2 pt-2 pb-4">
<div className="flex flex-wrap gap-2 justify-end">
{itemParticipants[index].map((participant, pIndex) => (
<span key={pIndex} className="flex items-center bg-[#389EFF]/30 text-[#0069CD] text-sm font-['Inter'] font-medium px-2 py-1 rounded-full border border-[#389EFF]">
<span className="truncate">{participant}</span>
<button
className="ml-1 text-[#0069CD] hover:text-[#004080] focus:outline-none cursor-pointer"
onClick={() => handleRemoveParticipant(index, participant)}
aria-label="참여자 삭제"
type="button"
>
×
</button>
</span>
))}
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</tbody>
</table>
</div>
);
};

export default ReceiptDetail;
9 changes: 0 additions & 9 deletions src/layouts/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,6 @@ const Layout = ({ children }: LayoutProps) => {
/>
<div className="text-[28px] font-black font-['Inter'] mt-[-8px]">PayCheck</div>
</div>

<div className="ml-auto flex gap-4 absolute right-8 top-5">
<button onClick={() => navigate(-1)} aria-label="뒤로">
<img src="/navigate_left.svg" alt="뒤로" className="w-10 h-10" />
</button>
<button onClick={() => navigate(1)} aria-label="앞으로">
<img src="/navigate_right.svg" alt="앞으로" className="w-10 h-10" />
</button>
</div>
</nav>
{/* 메인 컨텐츠 */}
<main className="flex-1 flex flex-col">{children}</main>
Expand Down
Loading