Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
75e1393
fix: use API settlement completion counts
yeoeun-ex May 17, 2026
8a96274
fix: refresh settlement header after payment update
yeoeun-ex May 17, 2026
57ea221
Merge remote-tracking branch 'origin/develop' into fix/MD-41
yeoeun-ex May 17, 2026
cfa0768
fix: align group header mock response
yeoeun-ex May 17, 2026
cef8d6c
fix: stabilize completed settlement flow
yeoeun-ex May 17, 2026
f63eb99
fix: refresh participant payment state
yeoeun-ex May 17, 2026
677a7b5
design: EllipsisVertical(⋮) 아이콘 추가
yoouyeon May 31, 2026
3bd495a
refactor: paymentRequestId 타입 추가
yoouyeon May 31, 2026
2d08ed4
refactor: expenseDetailLoader에 paymentRequestId 임시 연결
yoouyeon May 31, 2026
336e48c
design: Figma 기반으로 ExpenseMemberItem UI 재구현
yoouyeon May 31, 2026
54abc02
design: ExpenseMemberItem gap, padding 수정
yoouyeon May 31, 2026
369c47c
feat: 요청확인, 거절 버튼에 API 연결
yoouyeon May 31, 2026
7252f5c
fix: Member 타입 변경에 따른 mock 데이터 누락 필드 추가
yoouyeon May 31, 2026
bd4503d
merge: develop 브랜치 병합
yoouyeon May 31, 2026
41a2462
remove: 게스트 로그인 로직 제거
yoouyeon May 31, 2026
1a6bdc2
remove: select-group 페이지 제거
yoouyeon May 31, 2026
b21f315
refactor: member-expenses paymentRequestId 임시 우회 코드 제거
yoouyeon May 31, 2026
f6be4ae
fix: derive settlement completion from api state
yeoeun-ex May 31, 2026
8b506e0
feat: complete settlement manually
yeoeun-ex May 31, 2026
d69c65b
fix: skip complete api for finished settlements
yeoeun-ex May 31, 2026
0a3063f
fix: restrict manual settlement completion to managers
yeoeun-ex May 31, 2026
610489a
멤버 집계를 GroupHeader 기반으로 변경 (#56)
ongheong May 31, 2026
5769baf
fix: useApiError handleError가 ErrorBoundary 에러에도 실행되는 문제 수정
yoouyeon Jun 6, 2026
2d76954
refactor: useUpdatePaymentStatus useMutationWithHandlers 적용
yoouyeon Jun 6, 2026
fe07a1c
fix: 정산 상태 변경 확인 버튼 중복 제출 방지
yoouyeon Jun 6, 2026
a2c132c
refactor: status → settlementStatus 리네임
yoouyeon Jun 6, 2026
d73476a
fix: 정산 완료 상태에서 케밥 버튼 숨김 처리
yoouyeon Jun 6, 2026
378841b
fix: 입금 확인 요청 실패 시 토스트 표시, 모달 닫기
yoouyeon Jun 6, 2026
991fcdd
fix: RouteErrorBoundary 에러 상태 리셋 처리
yoouyeon Jun 6, 2026
1f28358
Merge branch 'develop' into feat/MD-42
yoouyeon Jun 6, 2026
6b947c7
feat: paymentRequestStatus 기반 정산 칩 상태 표시
yoouyeon Jun 6, 2026
0625c66
merge: 정산 상세 페이지에 입금확인/거절 UI 추가 (#57)
yoouyeon Jun 6, 2026
2bcf569
fix: CharacterItem 이미지 필드를 imageBigUrl로 수정
yoouyeon Jun 6, 2026
ca7385c
style: 주석을 실제 동작에 맞게 수정
yoouyeon Jun 6, 2026
9fb4fb7
merge: select-group 페이지, 게스트 로그인 로직 제거 (#59)
yoouyeon Jun 6, 2026
d11cfcb
refactor: CHARACTER_DATA 제거, 캐릭터 이미지 URL 직접 사용으로 전환
yoouyeon Jun 6, 2026
34f58b8
feat: 파일명 정제 함수 추가
yoouyeon Jun 6, 2026
87f577f
feat: editExpenses 라우트, 로더, 페이지 추가
yoouyeon Jun 6, 2026
823e41a
feat: expenseDetail에 ManageMenu 추가
yoouyeon Jun 6, 2026
4d6df19
design: disabled 버튼에 cursor pointer 제거
yoouyeon Jun 6, 2026
1e3141f
fix: StarChip 비활성 별 색상을 fill.alternative로 교체
yoouyeon Jun 7, 2026
ca3b07f
feat: NameChip onClick 지원, ExpenseAmountInput 퀵추가 버튼을 NameChip으로 교체
yoouyeon Jun 7, 2026
316c85d
refactor: NameChip을 Chip으로 리네이밍
yoouyeon Jun 7, 2026
2d77e2c
storybook: Chip AsButton 스토리 추가
yoouyeon Jun 7, 2026
109bc0e
fix: 잠긴 캐릭터 카드 border 토큰 적용, opacity, 아이콘 색상 수정
yoouyeon Jun 7, 2026
4c1e01b
fix: ExpenseMemberItem 비활성 색상을 fg.normal-disable로 교체
yoouyeon Jun 7, 2026
d30f19c
refactor: Chip black variant HACK 주석 제거
yoouyeon Jun 7, 2026
cc9aa94
feat: Input helpText 추가, error 상태 danger 아이콘 표시
yoouyeon Jun 7, 2026
d0185ef
design: 버튼 디자인 수정
yoouyeon Jun 7, 2026
0f7f4d7
feat: Dialog에 children 슬롯과 action disabled 지원 추가
yeoeun-ex Jun 7, 2026
f56b497
docs: 버튼 스토리 수정
yoouyeon Jun 7, 2026
abebbba
feat: Dropdown 컴포넌트 추가
yeoeun-ex Jun 7, 2026
8896129
feat: 계좌 수정 다이얼로그(AccountEditDialog) 추가
yeoeun-ex Jun 7, 2026
6370c8c
feat: expenseDetail 관리 메뉴에 계좌 수정 연결 및 header 갱신
yeoeun-ex Jun 7, 2026
09b0c5a
chore: main 브랜치 push 시 Storybook 자동 배포 트리거 추가
yoouyeon Jun 7, 2026
4707cb6
feat: add EmptySettlementCard component
yeoeun-ex Jun 7, 2026
4cfd9dd
feat: render EmptySettlementCard for empty settlements on home
yeoeun-ex Jun 7, 2026
b551cbd
merge: 정산 내역 수정 기능 추가 (#61)
yoouyeon Jun 7, 2026
1fad5f8
정산 상세 관리 메뉴에 계좌 수정 기능 추가 (#63)
ongheong Jun 7, 2026
6175ce1
빈 정산 카드UI 추가 및 홈 노출 (#64)
ongheong Jun 7, 2026
41b466b
merge: 캐릭터 하드코딩 데이터 대신 서버의 데이터를 사용하도록 수정 (#60)
yoouyeon Jun 7, 2026
fdf443a
merge: 디자인 시스템 피그마 코멘트 반영, HACK 주석 해소 (#62)
yoouyeon Jun 7, 2026
e0f4584
fix: 배포 환경에서 캐릭터 이미지 저장 실패 해결
yeoeun-ex Jun 7, 2026
6c03137
Merge remote-tracking branch 'origin/develop' into fix/image-test
yeoeun-ex Jun 7, 2026
f88a268
revert: 캐릭터 이미지 crossOrigin 제거
yeoeun-ex Jun 7, 2026
cdff33e
배포 환경에서 캐릭터 이미지 저장 실패 해결 (#66)
ongheong Jun 7, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/publish-storybook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: Publish Storybook
# https://www.chromatic.com/docs/github-actions/

on:
push:
branches:
- main
workflow_dispatch:
inputs:
pr_number:
Expand Down
5 changes: 5 additions & 0 deletions public/svgs/icon/ellipsis_vertical.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 65 additions & 0 deletions src/_workspace/AccountEditDialog/AccountEditDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { ChangeEventHandler } from 'react';
import { Dialog, Input, Modal } from '@/shared/design-system/ui';
import { Dropdown, type DropdownOption } from '../Dropdown';

interface AccountEditDialogProps {
open: boolean;
bankOptions: DropdownOption[];
bankValue: string;
bankPlaceholder?: string;
onBankChange: (value: string) => void;
accountNumber: string;
accountNumberPlaceholder?: string;
onAccountNumberChange: ChangeEventHandler<HTMLInputElement>;
onCancel: () => void;
onConfirm: () => void;
}

function AccountEditDialog(props: AccountEditDialogProps) {
const {
open,
bankOptions,
bankValue,
bankPlaceholder = '은행을 선택해주세요',
onBankChange,
accountNumber,
accountNumberPlaceholder = '계좌 번호를 입력해주세요',
onAccountNumberChange,
onCancel,
onConfirm,
} = props;

const isConfirmDisabled = !bankValue || !accountNumber;

return (
<Modal open={open} onClose={onCancel} ariaLabel="계좌 수정">
<Dialog
title="계좌 수정"
description="정산 받을 계좌를 입력해주세요."
mainAction={{
label: '확인',
onClick: onConfirm,
disabled: isConfirmDisabled,
}}
alternativeAction={{ label: '취소', onClick: onCancel }}
>
<Dropdown
label="은행 선택"
options={bankOptions}
value={bankValue}
onChange={onBankChange}
placeholder={bankPlaceholder}
/>
<Input
label="계좌 번호"
placeholder={accountNumberPlaceholder}
value={accountNumber}
onChange={onAccountNumberChange}
/>
</Dialog>
</Modal>
);
}

export { AccountEditDialog };
export type { AccountEditDialogProps };
2 changes: 2 additions & 0 deletions src/_workspace/AccountEditDialog/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AccountEditDialog } from './AccountEditDialog';
export type { AccountEditDialogProps } from './AccountEditDialog';
130 changes: 130 additions & 0 deletions src/_workspace/Dropdown/Dropdown.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import styled, { css } from 'styled-components';
import { getToken, applyTypography } from '@/shared/design-system';

export const Container = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: ${getToken('gap.4')};
width: 100%;
`;

export const Label = styled.span`
${applyTypography('typography.body.small-semibold')}
color: ${getToken('fg.neutral')};
`;

export const TriggerWrap = styled.div`
position: relative;
width: 100%;
`;

export const Trigger = styled.button<{ $isOpen: boolean }>`
display: flex;
align-items: center;
gap: ${getToken('gap.4')};
width: 100%;
padding: ${getToken('padding.4')} ${getToken('padding.5')};
background: ${getToken('fill.normal')};
border-radius: ${getToken('radius.lg')};
cursor: pointer;
${({ $isOpen }) =>
$isOpen
? css`
border: 2px solid ${getToken('border.primary.normal')};
`
: css`
border: 1px solid ${getToken('border.neutral')};
`}
`;

export const ValueText = styled.span<{ $isPlaceholder: boolean }>`
flex: 1;
min-width: 0;
text-align: left;
${applyTypography('typography.body.medium')}
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
${({ $isPlaceholder }) =>
$isPlaceholder
? css`
color: ${getToken('fg.assistive')};
opacity: 0.5;
`
: css`
color: ${getToken('fg.normal')};
`}
`;

export const ChevronWrapper = styled.span<{ $isOpen: boolean }>`
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: ${getToken('fg.assistive')};
transform: ${({ $isOpen }) => ($isOpen ? 'rotate(180deg)' : 'rotate(0deg)')};
transition: transform 0.2s ease-in-out;
`;

export const Panel = styled.div`
position: absolute;
top: calc(100% + ${getToken('gap.3')});
left: 0;
right: 0;
display: flex;
flex-direction: column;
gap: ${getToken('gap.1')};
background: ${getToken('fill.normal')};
border: 1px solid ${getToken('border.alternative')};
border-radius: ${getToken('radius.md')};
padding: ${getToken('padding.3')};
/* HACK: shadow2(0px 4px 8px rgba(0,0,0,0.12)) 매핑되는 shadow 시맨틱 토큰 없음 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
/* HACK: 옵션이 많을 때 패널이 넘치지 않도록 max-height 제한. 대응 토큰 없어 직접 사용. */
max-height: 240px;
overflow-y: auto;
z-index: 10;
`;

export const OptionItem = styled.button`
display: flex;
align-items: center;
justify-content: space-between;
gap: ${getToken('gap.6')};
width: 100%;
padding: ${getToken('padding.3')};
border-radius: ${getToken('radius.sm')};
background: transparent;
border: none;
cursor: pointer;

&:hover,
&:active {
background: ${getToken('fill.normal-pressed')};
}
`;

export const OptionLabel = styled.span<{ $isSelected: boolean }>`
${({ $isSelected }) =>
$isSelected
? applyTypography('typography.body.medium-semibold')
: applyTypography('typography.body.medium')}
color: ${({ $isSelected }) =>
$isSelected ? getToken('fg.primary.normal') : getToken('fg.normal')};
flex: 1;
min-width: 0;
text-align: left;
white-space: nowrap;
`;

export const ConfirmIconWrapper = styled.span`
display: flex;
align-items: center;
flex-shrink: 0;
color: ${getToken('fg.primary.normal')};

svg path {
stroke: currentColor;
}
`;
104 changes: 104 additions & 0 deletions src/_workspace/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useState, useRef, useEffect, useId } from 'react';
import SvgArrowDown from '@/shared/assets/svgs/icon/ArrowDown';
import SvgConfirm from '@/shared/assets/svgs/icon/Confirm';
import * as S from './Dropdown.styles';

interface DropdownOption {
label: string;
value: string;
}

interface DropdownProps {
label?: string;
options: DropdownOption[];
value: string;
onChange: (value: string) => void;
placeholder?: string;
}

function Dropdown(props: DropdownProps) {
const { label, options, value, onChange, placeholder = '선택' } = props;
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const listboxId = useId();

const selectedOption = options.find((option) => option.value === value);

useEffect(() => {
if (!isOpen) {
return undefined;
}

function handleOutsideClick(e: MouseEvent) {
if (!containerRef.current?.contains(e.target as Node)) {
setIsOpen(false);
}
}

function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
setIsOpen(false);
}
}

document.addEventListener('mousedown', handleOutsideClick);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('mousedown', handleOutsideClick);
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen]);

function handleOptionClick(optionValue: string) {
onChange(optionValue);
setIsOpen(false);
}

return (
<S.Container ref={containerRef}>
{label && <S.Label>{label}</S.Label>}
<S.TriggerWrap>
<S.Trigger
type="button"
$isOpen={isOpen}
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-controls={isOpen ? listboxId : undefined}
onClick={() => setIsOpen((prev) => !prev)}
>
<S.ValueText $isPlaceholder={!selectedOption}>
{selectedOption?.label ?? placeholder}
</S.ValueText>
<S.ChevronWrapper $isOpen={isOpen}>
<SvgArrowDown width={24} height={24} />
</S.ChevronWrapper>
</S.Trigger>
{isOpen && (
<S.Panel id={listboxId} role="listbox">
{options.map((option) => (
<S.OptionItem
key={option.value}
type="button"
role="option"
aria-selected={option.value === value}
onClick={() => handleOptionClick(option.value)}
>
<S.OptionLabel $isSelected={option.value === value}>
{option.label}
</S.OptionLabel>
{option.value === value && (
<S.ConfirmIconWrapper>
<SvgConfirm width={14} height={10} />
</S.ConfirmIconWrapper>
)}
</S.OptionItem>
))}
</S.Panel>
)}
</S.TriggerWrap>
</S.Container>
);
}

export { Dropdown };
export type { DropdownProps, DropdownOption };
2 changes: 2 additions & 0 deletions src/_workspace/Dropdown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Dropdown } from './Dropdown';
export type { DropdownProps, DropdownOption } from './Dropdown';
27 changes: 24 additions & 3 deletions src/app/RouteErrorBoundary/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { useLocation } from 'react-router';
import { ErrorPage } from '@/pages/error';
import { BoundaryError } from '@/shared/types/error.type';

type FallbackPageProps = Omit<FallbackProps, 'error'> & {
error: BoundaryError;
};

function FallbackPage({ error }: FallbackPageProps) {
function FallbackPage({ error, resetErrorBoundary }: FallbackPageProps) {
const { title, description, action } = error;

return <ErrorPage title={title} description={description} action={action} />;
return (
<ErrorPage
title={title}
description={description}
action={action}
onReset={resetErrorBoundary}
/>
);
}

interface RouteErrorBoundaryProps {
children?: React.ReactNode;
}

function RouteErrorBoundary({ children }: RouteErrorBoundaryProps) {
const location = useLocation();

return (
<ErrorBoundary FallbackComponent={FallbackPage}>{children}</ErrorBoundary>
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
FallbackComponent={FallbackPage}
onReset={reset}
resetKeys={[location.key]}
>
{children}
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}

Expand Down
Loading
Loading