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
46 changes: 0 additions & 46 deletions docs/balance-game-tag-filter-request.md

This file was deleted.

65 changes: 51 additions & 14 deletions src/api/client/api-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import type {
} from 'axios';

const shouldLog = process.env.NODE_ENV !== 'production';
const shouldLogPayloads =
shouldLog && process.env.NEXT_PUBLIC_API_LOG_PAYLOADS === 'true';
const MAX_LOG_LENGTH = 500;

const normalizeUrl = (config: InternalAxiosRequestConfig) => {
const base = config.baseURL ?? '';
Expand All @@ -16,26 +19,60 @@ const normalizeUrl = (config: InternalAxiosRequestConfig) => {
return `${base.replace(/\/$/, '')}/${url.replace(/^\//, '')}`;
};

const stringifyParams = (params: InternalAxiosRequestConfig['params']) => {
if (!params) return '';
const truncate = (value: string) =>
value.length > MAX_LOG_LENGTH
? `${value.slice(0, MAX_LOG_LENGTH)}...`
: value;

const sanitizeValue = (value: unknown): unknown => {
const seen = new WeakSet<object>();

const walk = (input: unknown): unknown => {
if (input === null || input === undefined) return input;

if (typeof input === 'string') return truncate(input);
if (typeof input === 'number' || typeof input === 'boolean') return input;
if (typeof input === 'bigint') return truncate(input.toString());

if (Array.isArray(input)) {
return input.slice(0, 50).map((item) => walk(item));
}

if (typeof input === 'object') {
const obj = input as object;
if (seen.has(obj)) return '[Circular]';
seen.add(obj);

const entries = Object.entries(obj as Record<string, unknown>);

return Object.fromEntries(entries.map(([key, val]) => [key, walk(val)]));
}

return truncate(String(input));
};

return walk(value);
};

const safeStringify = (value: unknown) => {
try {
return JSON.stringify(params);
return JSON.stringify(sanitizeValue(value));
} catch {
return '';
return truncate(String(value));
}
};

const stringifyParams = (params: InternalAxiosRequestConfig['params']) => {
if (!params) return '';

return safeStringify(params);
};

const stringifyData = (data: unknown) => {
if (data === undefined) return '';
if (typeof data === 'string') return truncate(data);

if (typeof data === 'string') return data;

try {
return JSON.stringify(data);
} catch {
return String(data);
}
return safeStringify(data);
};

export const attachApiLogger = (instance: AxiosInstance, label: string) => {
Expand All @@ -44,7 +81,7 @@ export const attachApiLogger = (instance: AxiosInstance, label: string) => {
instance.interceptors.request.use((config) => {
const method = (config.method || 'get').toUpperCase();
const url = normalizeUrl(config);
const params = stringifyParams(config.params);
const params = shouldLogPayloads ? stringifyParams(config.params) : '';

console.log(
`[API ${label}] ${method} ${url}${params ? ` params=${params}` : ''}`,
Expand All @@ -57,7 +94,7 @@ export const attachApiLogger = (instance: AxiosInstance, label: string) => {
(response) => {
const method = (response.config.method || 'get').toUpperCase();
const url = normalizeUrl(response.config);
const data = stringifyData(response.data);
const data = shouldLogPayloads ? stringifyData(response.data) : '';

console.log(`[API ${label}] ${method} ${url} -> ${response.status}`);
if (data) {
Expand All @@ -71,7 +108,7 @@ export const attachApiLogger = (instance: AxiosInstance, label: string) => {
const method = config?.method?.toUpperCase() || 'UNKNOWN';
const url = config ? normalizeUrl(config) : 'unknown';
const status = error.response?.status;
const data = stringifyData(error.response?.data);
const data = shouldLogPayloads ? stringifyData(error.response?.data) : '';

console.log(
`[API ${label}] ${method} ${url} -> ERROR${status ? ` ${status}` : ''}`,
Expand Down
6 changes: 5 additions & 1 deletion src/components/ui/filters/sort-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ export default function SortDropdown<T extends string>({

return (
<div className={cn('group relative', className)}>
<button className="rounded-100 bg-background-default border-border-subtle font-designer-14m text-text-default hover:bg-fill-neutral-subtle-hover flex items-center gap-50 border px-200 py-150 whitespace-nowrap transition-colors">
<button
type="button"
className="rounded-100 bg-background-default border-border-subtle font-designer-14m text-text-default hover:bg-fill-neutral-subtle-hover flex items-center gap-50 border px-200 py-150 whitespace-nowrap transition-colors"
>
{icon}
{label}
</button>
Expand All @@ -40,6 +43,7 @@ export default function SortDropdown<T extends string>({
>
{options.map((option) => (
<button
type="button"
key={option.value}
onClick={() => onChange(option.value)}
className="hover:bg-fill-neutral-subtle-hover font-designer-14r w-full px-200 py-150 text-left transition-colors"
Expand Down
3 changes: 3 additions & 0 deletions src/components/ui/filters/view-mode-toggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function ViewModeToggle<T extends string>({
return (
<button
key={option.value}
type="button"
onClick={() => onChange(option.value)}
className={cn(
'rounded-75 p-100 transition-colors',
Expand All @@ -41,6 +42,8 @@ export default function ViewModeToggle<T extends string>({
: 'text-text-subtlest hover:text-text-subtle',
)}
title={option.title}
aria-label={option.title ?? option.value}
aria-pressed={isActive}
>
{option.icon}
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ui/modal-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function ModalShell({
<Modal.Content>
<Modal.Header className="border-border-default flex items-center justify-between border-b">
<Modal.Title>{title}</Modal.Title>
<Modal.Close>
<Modal.Close aria-label="닫기">
<XIcon />
</Modal.Close>
</Modal.Header>
Expand Down
2 changes: 1 addition & 1 deletion src/components/voting/voting-create-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function VotingCreateForm({ onClose, onSubmit }: VotingCreateFormProps) {
const trimmedTagQuery = debouncedTagQuery.trim();
const { data: tagSuggestions = [], isFetching: isTagLoading } =
useBalanceGameTagSuggestionsQuery(trimmedTagQuery, {
limit: 10,
size: 10,
enabled: trimmedTagQuery.length >= BALANCE_GAME_TAG_MIN_QUERY_LEN,
minLength: BALANCE_GAME_TAG_MIN_QUERY_LEN,
sort: 'popular',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const isArchiveSearchSuggestionResponse = (
export const getArchiveSearchSuggestions = async ({
q,
minLength = 1,
limit = 10,
size = 10,
}: GetArchiveSearchSuggestionsParams): Promise<ArchiveSearchSuggestionResponse> => {
const response = await axiosInstance.get<
| ArchiveSearchSuggestionResponse
Expand All @@ -27,7 +27,7 @@ export const getArchiveSearchSuggestions = async ({
message?: string;
}
>('/archive/suggestions', {
params: { q, minLength, limit },
params: { q, minLength, size },
});

const payload =
Expand Down
2 changes: 2 additions & 0 deletions src/features/study/one-to-one/archive/api/update-archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { axiosInstance } from '@/api/client/axios';
export interface UpdateArchiveRequest {
title?: string;
description?: string;
link?: string;
isPrivate?: boolean;
}

export interface UpdateArchiveResponse {
id: number;
title: string;
description: string;
link: string;
isPrivate: boolean;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const ARCHIVE_QUERY_KEYS = {
[...ARCHIVE_QUERY_KEYS.all, 'search-suggestions'] as const,
searchSuggestionList: (params: {
q: string;
limit: number;
size: number;
minLength: number;
}) => [...ARCHIVE_QUERY_KEYS.searchSuggestions(), params] as const,
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ import { ARCHIVE_QUERY_KEYS } from '@/features/study/one-to-one/archive/model/ar
export const useArchiveSearchSuggestionsQuery = (
query: string,
options?: {
limit?: number;
size?: number;
minLength?: number;
enabled?: boolean;
},
) => {
const limit = options?.limit ?? 10;
const size = options?.size ?? 10;
const minLength = options?.minLength ?? 1;
const enabled = options?.enabled ?? query.trim().length >= minLength;

return useQuery({
queryKey: ARCHIVE_QUERY_KEYS.searchSuggestionList({
q: query,
limit,
size,
minLength,
}),
queryFn: () => getArchiveSearchSuggestions({ q: query, limit, minLength }),
queryFn: () => getArchiveSearchSuggestions({ q: query, size, minLength }),
enabled,
staleTime: 60_000,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function ArchiveFilters({
debouncedSearchTerm,
{
minLength: minQueryLength,
limit: 10,
size: 10,
enabled: isOpen && debouncedSearchTerm.trim().length >= minQueryLength,
},
);
Expand Down
15 changes: 14 additions & 1 deletion src/features/study/one-to-one/archive/ui/archive-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,30 @@ const LibraryCard = ({
const [isEditing, setIsEditing] = React.useState(false);
const [title, setTitle] = React.useState(item.title);
const [description, setDescription] = React.useState(item.description ?? '');
const [link, setLink] = React.useState(item.link ?? '');
const [nextPrivate, setNextPrivate] = React.useState(!!item.isPrivate);

React.useEffect(() => {
if (!isEditing) {
setTitle(item.title);
setDescription(item.description ?? '');
setLink(item.link ?? '');
setNextPrivate(!!item.isPrivate);
}
}, [isEditing, item.title, item.description, item.isPrivate]);
}, [isEditing, item.title, item.description, item.link, item.isPrivate]);

const handleSave = (e: React.MouseEvent) => {
e.stopPropagation();
const request: UpdateArchiveRequest = {};
const trimmedTitle = title.trim();
const trimmedDesc = description.trim();
const trimmedLink = link.trim();
const currentDesc = item.description ?? '';
const currentLink = item.link ?? '';

if (trimmedTitle !== item.title) request.title = trimmedTitle;
if (trimmedDesc !== currentDesc) request.description = trimmedDesc;
if (trimmedLink !== currentLink) request.link = trimmedLink;
if (nextPrivate !== !!item.isPrivate) request.isPrivate = nextPrivate;

if (Object.keys(request).length === 0) {
Expand All @@ -77,6 +82,7 @@ const LibraryCard = ({
setIsEditing(false);
setTitle(item.title);
setDescription(item.description);
setLink(item.link ?? '');
setNextPrivate(!!item.isPrivate);
};

Expand Down Expand Up @@ -127,6 +133,13 @@ const LibraryCard = ({
className="h-[96px]"
maxLength={100}
/>
<BaseInput
id={`archive-link-${item.id}`}
value={link}
onChange={(e) => setLink(e.target.value)}
placeholder="링크"
className="w-full"
/>
</>
) : (
<>
Expand Down
15 changes: 14 additions & 1 deletion src/features/study/one-to-one/archive/ui/archive-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,30 @@ const LibraryRow = ({
const [isEditing, setIsEditing] = React.useState(false);
const [title, setTitle] = React.useState(item.title);
const [description, setDescription] = React.useState(item.description ?? '');
const [link, setLink] = React.useState(item.link ?? '');
const [nextPrivate, setNextPrivate] = React.useState(!!item.isPrivate);

React.useEffect(() => {
if (!isEditing) {
setTitle(item.title);
setDescription(item.description ?? '');
setLink(item.link ?? '');
setNextPrivate(!!item.isPrivate);
}
}, [isEditing, item.title, item.description, item.isPrivate]);
}, [isEditing, item.title, item.description, item.link, item.isPrivate]);

const handleSave = (e: React.MouseEvent) => {
e.stopPropagation();
const request: UpdateArchiveRequest = {};
const trimmedTitle = title.trim();
const trimmedDesc = description.trim();
const trimmedLink = link.trim();
const currentDesc = item.description ?? '';
const currentLink = item.link ?? '';

if (trimmedTitle !== item.title) request.title = trimmedTitle;
if (trimmedDesc !== currentDesc) request.description = trimmedDesc;
if (trimmedLink !== currentLink) request.link = trimmedLink;
if (nextPrivate !== !!item.isPrivate) request.isPrivate = nextPrivate;

if (Object.keys(request).length === 0) {
Expand All @@ -77,6 +82,7 @@ const LibraryRow = ({
setIsEditing(false);
setTitle(item.title);
setDescription(item.description ?? '');
setLink(item.link ?? '');
setNextPrivate(!!item.isPrivate);
};

Expand Down Expand Up @@ -127,6 +133,13 @@ const LibraryRow = ({
className="h-[96px]"
maxLength={100}
/>
<BaseInput
id={`archive-link-${item.id}`}
value={link}
onChange={(e) => setLink(e.target.value)}
placeholder="링크"
className="w-full"
/>
<div className="flex items-center justify-between gap-150">
<div className="font-designer-12r text-text-subtle flex items-center gap-100">
<UserProfileModal
Expand Down
Loading
Loading