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
9 changes: 5 additions & 4 deletions src/apis/chatApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
CreateChatPayload,
DeleteChatApiResponse,
} from '@/types/api/chatApi';
import type { EntityId } from '@/types/id';

export const fetchChats = async (): Promise<ChatRoom[]> => {
const response = await clientApiClient<ChatListApiResponse>('/api/chats');
Expand Down Expand Up @@ -41,7 +42,7 @@ export const createChat = async (payload: CreateChatPayload): Promise<ChatRoom>
return response.data;
};

export const deleteChat = async (id: number): Promise<DeleteChatApiResponse> => {
export const deleteChat = async (id: EntityId): Promise<DeleteChatApiResponse> => {
const response = await clientApiClient<DeleteChatApiResponse>(`/api/chats/${id}`, {
method: 'DELETE',
});
Expand All @@ -54,9 +55,9 @@ export const deleteChat = async (id: number): Promise<DeleteChatApiResponse> =>
};

type FetchChatMessagesParams = {
chatId: number;
chatId: EntityId;
size?: number;
lastId?: number | null;
lastId?: EntityId | null;
};

export const fetchChatMessages = async ({
Expand Down Expand Up @@ -84,7 +85,7 @@ export const fetchChatMessages = async ({
};

export const addMessageFeedback = async (
messageId: number,
messageId: EntityId,
payload: AddMessageFeedbackPayload
): Promise<AddMessageFeedbackApiResponse> => {
const response = await clientApiClient<AddMessageFeedbackApiResponse>(
Expand Down
28 changes: 13 additions & 15 deletions src/apis/chatSocket.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fetchSocketAuthState, isSocketAuthFailure } from '@/lib/client/socketAuth';
import type { EntityId } from '@/types/id';
import {
Client,
type IFrame,
Expand All @@ -20,7 +21,7 @@ const withAuthorizationHeader = (authorization: string | null): StompHeaders =>
type Callback = () => void;

export type ChatSocketLink = {
linkId: number;
linkId: EntityId;
title: string;
url: string;
imageUrl: string | null;
Expand All @@ -29,16 +30,16 @@ export type ChatSocketLink = {

export type ChatSocketMessage = {
success: boolean;
chatId: number;
messageId: number | null;
chatId: EntityId;
messageId: EntityId | null;
content: string;
isEnd: boolean;
step: string | string[] | null;
links: ChatSocketLink[] | null;
};

export type ChatSocketOptions = {
chatId: string | number;
chatId: EntityId;
useSockJS?: boolean;
onMessage: (payload: ChatSocketMessage) => void;
onError?: (err: unknown) => void;
Expand Down Expand Up @@ -72,12 +73,9 @@ const toWebSocketUrl = (url: string) => {
return url.replace(/^http(s?):\/\//i, (_, secure) => `ws${secure ?? ''}://`);
};

const toNumberOrNull = (value: unknown) => {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string') {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
const toEntityIdOrNull = (value: unknown): EntityId | null => {
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
if (typeof value === 'string' && value.trim().length > 0) return value;
return null;
};

Expand All @@ -96,7 +94,7 @@ const toLinksOrNull = (value: unknown): ChatSocketLink[] | null => {
.map(item => {
if (!item || typeof item !== 'object') return null;
const link = item as Record<string, unknown>;
const linkId = toNumberOrNull(link.linkId) ?? toNumberOrNull(link.id);
const linkId = toEntityIdOrNull(link.linkId) ?? toEntityIdOrNull(link.id);
const title = typeof link.title === 'string' ? link.title : '';
const url = typeof link.url === 'string' ? link.url : '';
if (linkId === null || !title || !url) return null;
Expand All @@ -122,13 +120,13 @@ const parseIncomingMessage = (rawBody: string): ChatSocketMessage => {
throw new Error('Invalid socket payload: success is missing.');
}

const chatId = toNumberOrNull(data.chatId);
const chatId = toEntityIdOrNull(data.chatId);
if (chatId === null) {
throw new Error('Invalid socket payload: chatId is missing.');
}

const content = typeof data.content === 'string' ? data.content : '';
const messageId = toNumberOrNull(data.messageId);
const messageId = toEntityIdOrNull(data.messageId);
const isEnd = typeof data.isEnd === 'boolean' ? data.isEnd : false;
const step = toStepOrNull(data.step);
const links = toLinksOrNull(data.links);
Expand Down Expand Up @@ -323,7 +321,7 @@ export const createChatSocket = (options: ChatSocketOptions): ChatSocket => {

const send = async (message: string) => {
await ensureReadyToPublish();
const body = JSON.stringify({ chatId: Number(chatId), message });
const body = JSON.stringify({ chatId: String(chatId), message });
logWsDebug('send', { destination: SEND_DEST, body });
client.publish({
destination: SEND_DEST,
Expand All @@ -334,7 +332,7 @@ export const createChatSocket = (options: ChatSocketOptions): ChatSocket => {

const cancel = async () => {
await ensureReadyToPublish();
const body = JSON.stringify({ chatId: Number(chatId) });
const body = JSON.stringify({ chatId: String(chatId) });
logWsDebug('cancel', { destination: CANCEL_DEST, body });
client.publish({
destination: CANCEL_DEST,
Expand Down
23 changes: 12 additions & 11 deletions src/apis/linkApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ import type {
LinkSummaryStatusData,
SummaryStatusResponse,
} from '@/types/api/linkApi';
import type { EntityId } from '@/types/id';
import type { CreateLinkPayload, Link, LinkSummaryStatus, UpdateLinkPayload } from '@/types/link';

const LINKS_BFF = '/api/links';

export type LinkListParams = {
lastId?: number | null;
lastId?: EntityId | null;
size?: number;
};

Expand All @@ -39,7 +40,7 @@ const hasText = (value: string | null | undefined): value is string =>
typeof value === 'string' && value.trim().length > 0;

export const resolveSummaryContent = (
rawSummary: { id?: number; content?: string } | string | null | undefined
rawSummary: { id?: EntityId; content?: string } | string | null | undefined
): string => {
if (rawSummary !== null && typeof rawSummary === 'object') {
return typeof rawSummary.content === 'string' ? rawSummary.content : '';
Expand Down Expand Up @@ -86,10 +87,10 @@ function buildQuery(params?: LinkListParams) {
}

type LinkSource = {
id?: number;
id?: EntityId;
url?: string;
title?: string;
summary?: { id: number; content: string } | string | null;
summary?: { id: EntityId; content: string } | string | null;
summaryStatus?: unknown;
summaryErrorMessage?: string | null;
summaryProgress?: number | null;
Expand All @@ -110,7 +111,7 @@ const normalizeLink = (data: LinkSource): Link => {
);

return {
id: data.id ?? 0,
id: data.id ?? '',
url: data.url ?? '',
title: data.title ?? '',
summary,
Expand Down Expand Up @@ -169,7 +170,7 @@ export const createLink = async (payload: CreateLinkPayload): Promise<Link> => {
return normalizeLink(body.data);
};

export const fetchLink = async (id: number): Promise<Link> => {
export const fetchLink = async (id: EntityId): Promise<Link> => {
const body = await clientApiClient<LinkApiResponse>(`${LINKS_BFF}/${id}`);

if (!body?.data || !body.success) {
Expand All @@ -179,7 +180,7 @@ export const fetchLink = async (id: number): Promise<Link> => {
return normalizeLink(body.data);
};

export const updateLink = async (id: number, payload: UpdateLinkPayload): Promise<Link> => {
export const updateLink = async (id: EntityId, payload: UpdateLinkPayload): Promise<Link> => {
const body = await clientApiClient<LinkApiResponse>(`${LINKS_BFF}/${id}`, {
method: 'PUT',
body: JSON.stringify(payload),
Expand All @@ -192,7 +193,7 @@ export const updateLink = async (id: number, payload: UpdateLinkPayload): Promis
return normalizeLink(body.data);
};

export const updateLinkTitle = async (id: number, title: string): Promise<Link> => {
export const updateLinkTitle = async (id: EntityId, title: string): Promise<Link> => {
const body = await clientApiClient<LinkApiResponse>(`${LINKS_BFF}/${id}/title`, {
method: 'PATCH',
body: JSON.stringify({ title }),
Expand All @@ -205,7 +206,7 @@ export const updateLinkTitle = async (id: number, title: string): Promise<Link>
return normalizeLink(body.data);
};

export const updateLinkMemo = async (id: number, memo: string): Promise<Link> => {
export const updateLinkMemo = async (id: EntityId, memo: string): Promise<Link> => {
const body = await clientApiClient<LinkApiResponse>(`${LINKS_BFF}/${id}/memo`, {
method: 'PATCH',
body: JSON.stringify({ memo }),
Expand All @@ -218,7 +219,7 @@ export const updateLinkMemo = async (id: number, memo: string): Promise<Link> =>
return normalizeLink(body.data);
};

export const deleteLink = async (id: number): Promise<DeleteLinkApiResponse> => {
export const deleteLink = async (id: EntityId): Promise<DeleteLinkApiResponse> => {
const body = await clientApiClient<DeleteLinkApiResponse>(`${LINKS_BFF}/${id}`, {
method: 'DELETE',
});
Expand Down Expand Up @@ -257,7 +258,7 @@ export const scrapeLinkMeta = async (url: string) => {
return response.data;
};

export const fetchLinkSummaryStatus = async (id: number): Promise<LinkSummaryStatusData> => {
export const fetchLinkSummaryStatus = async (id: EntityId): Promise<LinkSummaryStatusData> => {
const body = await clientApiClient<LinkSummaryStatusApiResponse>(
`/api/links/${id}/summary-status`,
{
Expand Down
3 changes: 2 additions & 1 deletion src/apis/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import type {
SelectSummaryResponse,
SummaryResponse,
} from '@/types/api/summaryApi';
import type { EntityId } from '@/types/id';

export const retrySummary = async (id: number) => {
export const retrySummary = async (id: EntityId) => {
const body = await clientApiClient<RetrySummaryResponse>(`/api/links/${id}/retry-summary`, {
method: 'POST',
});
Expand Down
19 changes: 13 additions & 6 deletions src/apis/summarySocket.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { fetchSocketAuthState, isSocketAuthFailure } from '@/lib/client/socketAuth';
import type { EntityId } from '@/types/id';
import {
Client,
type IFrame,
Expand All @@ -26,19 +27,19 @@ const withAuthorizationHeader = (authorization: string | null): StompHeaders =>
authorization ? { Authorization: authorization } : {};

export type SummaryStatusPayload = {
linkId: number;
linkId: EntityId;
status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
progress?: number;
summary?: { id?: number; content?: string } | string | null;
summary?: { id?: EntityId; content?: string } | string | null;
errorMessage?: string;
updatedAt?: string;
data?: { id?: number; content?: string } | string | null;
data?: { id?: EntityId; content?: string } | string | null;
};

export type LinkSummaryStatus = 'idle' | 'generating' | 'ready' | 'failed';

export type SummaryStatusEvent = {
linkId: number;
linkId: EntityId;
status: LinkSummaryStatus;
progress?: number;
summary?: string;
Expand Down Expand Up @@ -91,14 +92,20 @@ const resolveSummaryText = (
return typeof rawSummary === 'string' ? rawSummary : undefined;
};

const toEntityIdOrNull = (value: unknown): EntityId | null => {
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
if (typeof value === 'string' && value.trim().length > 0) return value;
return null;
};

const parsePayload = (rawBody: string): SummaryStatusEvent => {
const payload = JSON.parse(rawBody) as SummaryStatusPayload;
if (typeof payload !== 'object' || payload === null) {
throw new Error('Invalid summary status event payload');
}

const linkId = Number(payload.linkId);
if (!Number.isFinite(linkId)) {
const linkId = toEntityIdOrNull(payload.linkId);
if (linkId === null) {
throw new Error('Invalid summary status event: missing linkId.');
}

Expand Down
5 changes: 3 additions & 2 deletions src/app/(dev)/chat-api-demo/ChatApiDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Label from '@/components/basics/Label/Label';
import TextArea from '@/components/basics/TextArea/TextArea';
import { useChatStream } from '@/hooks/server/Chats/useChatStream';
import type { ChatRoom } from '@/types/api/chatApi';
import type { EntityId } from '@/types/id';
import { useCallback, useEffect, useMemo, useState } from 'react';

const defaultForm = { firstChat: '' };
Expand Down Expand Up @@ -41,7 +42,7 @@ export default function ChatApiDemo() {
const [creating, setCreating] = useState(false);
const [createError, setCreateError] = useState<string | null>(null);

const [selectedChatId, setSelectedChatId] = useState<number | null>(null);
const [selectedChatId, setSelectedChatId] = useState<EntityId | null>(null);
const [streamLog, setStreamLog] = useState<StreamLogItem[]>([]);
const [question, setQuestion] = useState('');
const [streamEnabled, setStreamEnabled] = useState(false);
Expand All @@ -53,7 +54,7 @@ export default function ChatApiDemo() {
chatId: chatIdForSocket,
enabled: streamEnabled && Boolean(chatIdForSocket),
onMessage: payload => {
if (payload.chatId !== selectedChatId) return;
if (selectedChatId !== null && payload.chatId !== selectedChatId) return;

setStreamLog(prev => [
...prev,
Expand Down
19 changes: 8 additions & 11 deletions src/app/(dev)/link-api-demo/LinkApiDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useGetLinks } from '@/hooks/useGetLinks';
import { usePostLinks } from '@/hooks/usePostLinks';
import { useUpdateLinkMemo } from '@/hooks/useUpdateLinkMemo';
import { useUpdateLinkTitle } from '@/hooks/useUpdateLinkTitle';
import type { EntityId } from '@/types/id';
import type { Link } from '@/types/link';
import { useEffect, useMemo, useState } from 'react';

Expand Down Expand Up @@ -53,19 +54,15 @@ export default function LinkApiDemo() {
}
};

const handleDelete = async (id: number) => {
try {
await deleteMut.mutateAsync(id);
} catch {
// 에러는 LinkCardRow 혹은 deleteMut.isError를 통해 표시
}
const handleDelete = async (id: EntityId) => {
await deleteMut.mutateAsync(id);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const handleUpdateTitle = async (id: number, title: string) => {
const handleUpdateTitle = async (id: EntityId, title: string) => {
await updateTitleMut.mutateAsync({ id, title });
};

const handleUpdateMemo = async (id: number, memo: string) => {
const handleUpdateMemo = async (id: EntityId, memo: string) => {
await updateMemoMut.mutateAsync({ id, memo });
};

Expand Down Expand Up @@ -200,9 +197,9 @@ function LinkCardRow({
onUpdateMemo,
}: {
link: Link;
onDelete: (id: number) => Promise<void>;
onUpdateTitle: (id: number, title: string) => Promise<void>;
onUpdateMemo: (id: number, memo: string) => Promise<void>;
onDelete: (id: EntityId) => Promise<void>;
onUpdateTitle: (id: EntityId, title: string) => Promise<void>;
onUpdateMemo: (id: EntityId, memo: string) => Promise<void>;
}) {
const [nextTitle, setNextTitle] = useState(link.title);
const [nextMemo, setNextMemo] = useState(link.memo ?? '');
Expand Down
Loading
Loading