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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"react-dom": "19.2.4",
"react-icons": "^5.6.0",
"serwist": "^9.5.7",
"sonner": "^2.0.7",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"tailwind-merge": "^3.5.0",
"vaul": "^1.1.2",
"zustand": "^5.0.12"
Expand Down
1 change: 1 addition & 0 deletions packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"dependencies": {
"clsx": "^2.1.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
Expand Down
41 changes: 41 additions & 0 deletions packages/design-system/src/components/Toaster.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { Toaster as SonnerToaster, toast } from 'sonner';
import type { ToasterProps } from 'sonner';

export type AppToasterProps = ToasterProps;

const defaultToastOptions: NonNullable<ToasterProps['toastOptions']> = {
classNames: {
toast: 'rounded-2xl border border-border-soft bg-card text-foreground shadow-lg',
title: 'text-sm font-semibold text-foreground',
description: 'text-xs text-muted-foreground',
actionButton: 'rounded-full bg-foreground text-background',
cancelButton: 'rounded-full bg-muted text-foreground',
closeButton: 'border-border-soft bg-card text-muted-foreground',
},
};

export function AppToaster(props: AppToasterProps) {
const { toastOptions, ...toasterProps } = props;
const mergedToastOptions: ToasterProps['toastOptions'] = {
...defaultToastOptions,
...toastOptions,
classNames: {
...defaultToastOptions.classNames,
...toastOptions?.classNames,
},
};

return (
<SonnerToaster
position="bottom-center"
richColors
closeButton
{...toasterProps}
toastOptions={mergedToastOptions}
/>
);
}

export { toast };
3 changes: 3 additions & 0 deletions packages/design-system/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ export type { BottomSheetProps } from './components/BottomSheet';

export { Modal } from './components/Modal';
export type { ModalProps } from './components/Modal';

export { AppToaster, toast } from './components/Toaster';
export type { AppToasterProps } from './components/Toaster';
17 changes: 17 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from 'next';
import { AppToaster } from '@frontend/design-system';
import { NotificationSSEProvider } from '@/app/providers/notification-sse-provider';
import { QueryProvider } from '@/app/providers/query-provider';
import { ThemeProvider } from '@/app/providers/theme-provider';
Expand Down Expand Up @@ -52,6 +53,7 @@ export default function RootLayout({
<QueryProvider>
<NotificationSSEProvider />
{children}
<AppToaster />
</QueryProvider>
</ThemeProvider>
<PwaUpdatePrompt />
Expand Down
28 changes: 18 additions & 10 deletions src/features/auth/ui/login/SocialLoginButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
'use client';

import { toast } from '@frontend/design-system';
import { SiGoogle, SiNaver } from 'react-icons/si';

type SocialLoginButtonsProps = {
naverHref: string;
googleHref: string;
};

export function SocialLoginButtons({
naverHref,
googleHref,
}: SocialLoginButtonsProps) {
function showBlockedOAuth() {
toast.info('시연 중에는 소셜 로그인을 잠시 막아뒀어요.');
}

export function SocialLoginButtons(props: SocialLoginButtonsProps) {
void props;

return (
<>
<div className="relative flex items-center gap-3 text-xs font-semibold tracking-[0.2em] text-gray-300 uppercase">
Expand All @@ -18,20 +24,22 @@ export function SocialLoginButtons({
</div>

<div className="relative grid gap-3">
<a
href={naverHref}
<button
type="button"
onClick={showBlockedOAuth}
className="flex h-12 items-center justify-center gap-3 rounded-2xl border border-[#03C75A] bg-[#03C75A] px-4 text-sm font-semibold text-white transition hover:brightness-95"
>
<SiNaver size={18} />
네이버로 시작하기
</a>
<a
href={googleHref}
</button>
<button
type="button"
onClick={showBlockedOAuth}
className="flex h-12 items-center justify-center gap-3 rounded-2xl border border-gray-200 bg-white px-4 text-sm font-semibold text-gray-700 transition hover:border-brand-200 hover:bg-brand-50"
>
<SiGoogle size={18} className="text-[#EA4335]" />
구글로 시작하기
</a>
</button>
</div>
</>
);
Expand Down
6 changes: 3 additions & 3 deletions src/features/chat/api/chat-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ function toChatRoom(room: BackendRoom): ChatRoom {
room.title ??
(room.spotId ? `스팟 ${room.spotId}` : `팀 채팅 ${id}`),
subtitle: room.subtitle ?? '팀 채팅',
description: room.lastMessagePreview ?? '백엔드 채팅방입니다.',
description: room.lastMessagePreview ?? '아직 메시지가 없어요.',
metaLabel: '팀 채팅',
updatedAt,
unreadCount: room.unreadCount ?? 0,
Expand All @@ -134,7 +134,7 @@ function toChatRoom(room: BackendRoom): ChatRoom {
type: 'REQUEST',
status: 'OPEN',
title: room.spotId ? `스팟 ${room.spotId}` : `팀 채팅 ${id}`,
description: '백엔드 채팅방입니다.',
description: '아직 메시지가 없어요.',
pointCost: 0,
authorId: '',
authorNickname: '',
Expand Down Expand Up @@ -162,7 +162,7 @@ function toChatRoom(room: BackendRoom): ChatRoom {
counterpartRole: 'PARTNER',
title: room.title ?? `개인 채팅 ${id}`,
subtitle: room.subtitle ?? '개인 채팅',
description: room.lastMessagePreview ?? '백엔드 채팅방입니다.',
description: room.lastMessagePreview ?? '아직 메시지가 없어요.',
metaLabel: '개인 채팅',
updatedAt,
messages: [],
Expand Down
14 changes: 13 additions & 1 deletion src/features/feed/model/feed-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,27 @@ export type FeedMarkerFilter = {
feedType: 'all' | 'offer' | 'request';
categories: readonly SpotCategory[];
searchQuery: string;
excludeApplied?: boolean;
onlyOpenFeeds?: boolean;
};

export function filterVisibleFeedItems(
feedItems: readonly FeedItem[],
{ feedType, categories, searchQuery }: FeedMarkerFilter,
{
feedType,
categories,
searchQuery,
excludeApplied = false,
onlyOpenFeeds = false,
}: FeedMarkerFilter,
): FeedItem[] {
const q = searchQuery.trim().toLowerCase();

return feedItems.filter((item) => {
if (onlyOpenFeeds && (item.status !== 'OPEN' || item.spotId)) {
return false;
}
if (excludeApplied && isSearchExcludedFeedItem(item)) return false;
if (feedType === 'offer' && item.type !== 'OFFER') return false;
if (feedType === 'request' && item.type !== 'REQUEST') return false;
if (
Expand Down
2 changes: 1 addition & 1 deletion src/features/feed/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export interface FeedItem {
owner?: boolean;
/**
* 2026-04-30 — contextBuilder 시뮬레이션이 합성한 AI 피드 마커.
* 실제 호스트가 만든 글이 아니므로 참여 액션 대신 "리퀘스트 열기" 안내로 대체.
* 실제 호스트가 만든 글이 아니므로 참여 액션 대신 "알려줘 열기" 안내로 대체.
*/
isAi?: boolean;
// 2026-04-30 contextBuilder PlanV3/PriceBreakdown/Preparation/ResolvedPlace 통합.
Expand Down
40 changes: 29 additions & 11 deletions src/features/feed/ui/detail/FeedParticipationActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button, Input, Modal } from '@frontend/design-system';
import { Button, Input, Modal, toast } from '@frontend/design-system';
import { usePointBalance } from '@/features/pay';
import { BottomSheet } from '@/shared/ui';
import { useBottomNavMessageStore } from '@/shared/model/bottom-nav-message-store';
Expand Down Expand Up @@ -285,6 +285,7 @@ export function FeedParticipationActions({
'참여 신청이 완료되었어요. 승인되면 팀 채팅에 참여할 수 있어요.',
'',
);
toast.success('참여 신청이 완료되었어요.');
setSelectedRole(null);
router.refresh();
} finally {
Expand All @@ -295,6 +296,8 @@ export function FeedParticipationActions({
const hasPendingApplication =
item.myApplicationStatus === 'APPLIED' ||
item.myApplicationStatus === 'PENDING';
const hasActiveApplication =
hasPendingApplication || item.myApplicationStatus === 'ACCEPTED';

const cancelOutcome: CancellationOutcome | null = useMemo(() => {
if (
Expand Down Expand Up @@ -327,6 +330,7 @@ export function FeedParticipationActions({
await cancelFeedApplication.mutateAsync(item.id);

showBottomNavMessage('신청을 취소했어요.', '');
toast.success('신청을 취소했어요.');
setCancelOpen(false);
router.refresh();
} finally {
Expand Down Expand Up @@ -354,7 +358,7 @@ export function FeedParticipationActions({
return '신청 후 승인되면 팀 채팅에 참여할 수 있어요.';
})();

const isApplied = hasPendingApplication;
const isApplied = hasActiveApplication;
const pendingApplications = (applicationsQuery.data?.data ?? []).filter(
isPendingApplication,
);
Expand Down Expand Up @@ -512,25 +516,36 @@ export function FeedParticipationActions({
}

// AI 피드(시뮬레이터가 합성한 데이터) 는 실제 호스트가 없으므로 참여가 의미 없음.
// 같은 자리에 "이런 리퀘스트 직접 열기" 액션만 보여주고 /post/request 로 prefill 라우팅.
// 같은 자리에 "이런 알려줘 직접 열기" 액션만 보여주고 /post/request 로 prefill 라우팅.
if (item.isAi) {
const openRequestFromAiFeed = () => {
const params = new URLSearchParams({
fromSpot: item.id,
title: item.title,
category: item.category ?? '',
location: item.location,
content: item.description ?? item.title,
});
const lat = item.lat ?? item.coord?.lat;
const lng = item.lng ?? item.coord?.lng;
if (typeof lat === 'number') params.set('lat', String(lat));
if (typeof lng === 'number') params.set('lng', String(lng));
router.push(`/post/request?${params.toString()}`);
};

return (
<div className="fixed right-0 bottom-0 left-0 z-30 border-t border-border-soft bg-card/95 px-4 py-3 backdrop-blur-sm">
<p className="mb-2 text-center text-[11px] text-muted-foreground">
AI 가 추천한 모임이에요. 마음에 들면 비슷한 리퀘스트를 직접
AI가 추천한 모임이에요. 마음에 들면 비슷한 알려줘를 직접
열어보세요.
</p>
<Button
fullWidth
size="lg"
className="rounded-full bg-accent active:translate-y-px active:opacity-90"
onClick={() =>
router.push(
`/post/request?fromSpot=${encodeURIComponent(item.id)}`,
)
}
onClick={openRequestFromAiFeed}
>
리퀘스트 열기
알려줘 열기
</Button>
</div>
);
Expand All @@ -545,9 +560,12 @@ export function FeedParticipationActions({
size="lg"
variant="secondary"
className="rounded-full"
disabled={!hasPendingApplication}
onClick={() => setCancelOpen(true)}
>
신청 취소
{hasPendingApplication
? '신청 취소'
: '이미 참여 중인 피드예요'}
</Button>
) : (
<div className="grid grid-cols-2 gap-2">
Expand Down
Loading
Loading