API, database schema, and type definition reference
Stores user profile information.
| Column | Type | Description |
|---|---|---|
id |
string (UUID) | PK, Supabase Auth ID |
nickname |
string | Nickname (1-8 characters, alphanumeric/Korean) |
profile_image |
string | Profile image URL |
location |
LocationEnum | Activity region |
pos |
PositionEnum | Primary position |
genre |
GenreEnum | Preferred genre |
level |
LevelEnum | Skill level |
tags |
string[] | Tag array (#rock, #blues) |
bpm |
number | BPM (optional) |
is_bpm_public |
boolean | Whether BPM is public |
allow_noti |
boolean | Notifications allowed |
accept_marketing |
boolean | Consent to marketing communications |
status |
'a' | 'i' | Active/inactive |
TypeScript Interface:
// app/lib/interfaces/entity.interface.ts
interface Entity_User {
id?: string;
nickname?: string;
profile_image?: string;
location?: LocationEnum;
pos?: PositionEnum;
genre?: GenreEnum;
level?: LevelEnum;
tags?: string[];
bpm?: number;
is_bpm_public?: boolean;
allow_noti?: boolean;
status?: 'a' | 'i';
playlist: Entity_Playlist[];
}Stores jam session information.
| Column | Type | Description |
|---|---|---|
id |
number | PK, auto-increment |
user_id |
string | FK โ user.id (host) |
title |
string | Title (5-20 characters) |
body |
string | Description (max 1000 characters) |
location |
LocationEnum[] | Region (multiple selection) |
genre |
GenreEnum[] | Genre (multiple selection) |
level |
LevelEnum[] | Skill level (multiple selection) |
pos |
PositionEnum[] | Positions being recruited |
datetime |
string | Date and time (ISO8601) |
location_detail |
JSON | Detailed venue information |
headcnt_by_pos |
JSON[] | Number of members recruited per position |
songs |
JSON[] | Ensemble song list |
images |
string[] | Image URL array |
openchat |
string | Open chat link (optional) |
created_at |
string | Creation date and time |
JSON field details:
// location_detail
{
name: string; // Venue name
address: string; // Address
category: string; // Category
}
// headcnt_by_pos
[
{ pos: "๊ธฐํ", headcnt: 2 },
{ pos: "๋๋ผ", headcnt: 1 }
]
// songs
[
{ title: "๊ณก๋ช
", artist: "์ํฐ์คํธ", album_cover_url: "URL" }
]TypeScript Interface:
interface Entity_JamSession {
id?: number;
user_id?: string;
title?: string;
body?: string;
location?: LocationEnum[];
genre?: GenreEnum[];
level?: LevelEnum[];
pos?: PositionEnum[];
datetime?: string;
location_detail?: ZodType_LocationDetail;
headcnt_by_pos?: ZodType_HeadCnt[];
songs?: ZodType_Song[];
images?: string[];
openchat?: string;
created_at?: string;
// Computed fields (not DB columns)
curHeadCnt?: number; // Current number of participants
maxHeadCnt?: number; // Maximum number of members
appliedPosDict?: Record<string, number>; // Application count per position
// Relations
user?: Entity_User;
jamentry?: Entity_JamEntry[];
}Stores jam session application information.
| Column | Type | Description |
|---|---|---|
id |
number | PK, auto-increment |
jam_id |
number | FK โ jamsession.id |
user_id |
string | FK โ user.id |
appeal |
string | Self-introduction (HTML sanitized) |
applied_pos |
PositionEnum | Applied position |
is_allowed |
boolean | Whether approved |
is_rejam |
boolean | Whether re-jam (re-application) |
noshow |
boolean | Whether no-show |
TypeScript Interface:
interface Entity_JamEntry {
id: number;
jam_id: number;
user_id: string;
appeal: string;
applied_pos: PositionEnum;
is_allowed: boolean;
is_rejam: boolean;
noshow?: boolean;
user?: Entity_User;
}Stores user playlist information.
| Column | Type | Description |
|---|---|---|
id |
number | PK |
user_id |
string | FK โ user.id |
title |
string | Song title |
thumbnail_path |
string | Thumbnail URL |
author |
string | Artist |
date |
string | Date added |
is_public |
boolean | Whether public |
origin |
string | Source (spotify, etc.) |
Table used for nickname uniqueness checks.
| Column | Type | Description |
|---|---|---|
nickname |
string | PK, nickname |
Handles the OAuth callback
| Parameter | Type | Description |
|---|---|---|
code |
query | OAuth authorization code |
Response:
- Existing user โ redirect to
/jamsession - New user โ redirect to
/signup - Error โ redirect to
/auth/auth-code-error
File: app/api/auth/callback/route.ts
ManiaDB song search
Request:
{
"songName": "๊ฒ์์ด"
}Response:
{
"status": 200,
"msg": "Success",
"data": [
{
"title": "๊ณก๋ช
",
"artist": "์ํฐ์คํธ",
"album_cover_url": "URL"
}
]
}File: app/api/maniadb/route.ts
Naver Local Search API
Request:
{
"query": "์ฅ์ ๊ฒ์์ด"
}Response:
{
"status": 200,
"msg": "Success",
"data": [
{
"name": "์ฅ์๋ช
",
"address": "์ฃผ์",
"category": "์นดํ
๊ณ ๋ฆฌ"
}
]
}File: app/api/naver/map/route.ts
| Function | Parameters | Returns | Description |
|---|---|---|---|
createJamEntry |
CreateJamEntryDto |
Promise<boolean> |
Create an application |
| Function | Parameters | Returns | Description |
|---|---|---|---|
getUser |
supabase |
Promise<User> |
Retrieve the current user |
getJam |
id: number |
Promise<Entity_JamSession> |
Retrieve jam session details |
| Function | Parameters | Returns | Description |
|---|---|---|---|
deleteJamSession |
jam_id: number |
Promise<boolean> |
Delete a jam session |
deleteJamEntry |
entryId: number |
Promise<boolean> |
Delete an application |
| Function | Parameters | Returns | Description |
|---|---|---|---|
isNicknameUnique |
nickname: string |
Promise<boolean> |
Check nickname uniqueness |
| Function | Parameters | Returns | Description |
|---|---|---|---|
fetchJams |
{ pageParam, queryFnParam } |
Promise<{ data, nextPage }> |
Jam session list (pagination) |
| Function | Description |
|---|---|
createJamSession |
Create a jam session (including image upload) |
updateJamSession |
Update a jam session |
initialize |
Initialize the session detail page (permission check) |
| Function | Description |
|---|---|
allowEntry |
Approve an application |
refuseEntry |
Reject an application |
setNoShow |
Mark as no-show |
letOutMember |
Remove a participant |
GenreEnum (app/lib/enums/genre.enum.ts)
enum GenreEnum {
All = "์ข
ํฉ",
Rock = "๋ฝ",
Pop = "ํ",
Indie = "์ธ๋",
Jazz = "์ฌ์ฆ",
Punk = "ํํฌ",
Metal = "๋ฉํ",
}PositionEnum (app/lib/enums/position.enum.ts)
enum PositionEnum {
Guitar = "๊ธฐํ",
Base = "๋ฒ ์ด์ค",
Drum = "๋๋ผ",
Vocal = "๋ณด์ปฌ",
Keyboard = "ํค๋ณด๋",
ETC = "๊ทธ ์ธ",
}LevelEnum (app/lib/enums/level.enum.ts)
enum LevelEnum {
Pro = "ํ๋ก",
Amature = "์๋ง์ถ์ด",
Study = "์คํฐ๋",
Hobby = "์ทจ๋ฏธ",
}LocationEnum (app/lib/enums/location.enum.ts)
enum LocationEnum {
Everywhere = "์ ์ฒด",
SeoUl = "์์ธ",
GyeonGgiNorth = "๊ฒฝ๊ธฐ ๋ถ๋ถ",
GyeonGgiSouth = "๊ฒฝ๊ธฐ ๋จ๋ถ",
InCheon = "์ธ์ฒ",
BuSan = "๋ถ์ฐ",
DaeGu = "๋๊ตฌ",
DaeJeon = "๋์ ",
GwangJu = "๊ด์ฃผ",
UlSan = "์ธ์ฐ",
SeJong = "์ธ์ข
",
GangWon = "๊ฐ์",
ChungBuk = "์ถฉ๋ถ",
ChungNam = "์ถฉ๋จ",
JeonBuk = "์ ๋ถ",
JeonNam = "์ ๋จ",
GyeongBuk = "๊ฒฝ๋ถ",
GyeongNam = "๊ฒฝ๋จ",
JeJu = "์ ์ฃผ",
}UserStatus (app/lib/enums/user.enum.ts)
enum UserStatus {
Active = "a",
Inactive = "i",
}interface CreateJamDto {
user_id: string;
title: string;
body: string;
location: LocationEnum[];
pos: PositionEnum[];
genre: GenreEnum[];
level: LevelEnum[];
date: string;
headcnt_by_pos: number;
}
interface CreateJamEntryDto {
user_id: number | string;
jam_id: number | string;
appeal: string;
applied_pos: string;
}
interface CreatePlaylistItemDto {
user_id: string;
thumbnail_path: string;
title: string;
author: string;
date: string;
origin: string;
}interface FetchResult {
status: number; // 200: success, 500: failure
msg: string; // Status message
data: any; // Response data
}interface GlobalContext {
bodyWidth: number;
bodyHeight: number;
isSmallLoadingOn: boolean;
setSmallLoading: () => void;
bottomSheet: BottomSheetContext;
session: {
userProfile: UserProfile;
setUserProfile: (input: Partial<UserProfile>) => void;
};
resetSlideCalendarDots: boolean;
setResetSlideCalendarDots: Dispatch<SetStateAction<boolean>>;
}
interface BottomSheetContext {
children: ReactNode;
location: BottomSheetHeight;
set: (input: Partial<BottomSheetContext>) => void;
header?: boolean;
setHeader: null | ((input: boolean) => void);
}
interface UserProfile {
isLoaded: undefined | boolean;
accept_marketing: undefined | boolean;
profile_image: undefined | null | string;
nickname: undefined | string;
tags: undefined | string[];
pos: undefined | PositionEnum;
genre: undefined | GenreEnum;
level: undefined | LevelEnum;
location: undefined | LocationEnum;
}Mapping used to store values in abbreviated form in the database.
// Position
const MapDataToPosition = {
g: PositionEnum.Guitar,
b: PositionEnum.Base,
d: PositionEnum.Drum,
v: PositionEnum.Vocal,
k: PositionEnum.Keyboard,
e: PositionEnum.ETC,
};
const MapPositionToData = _.invert(MapDataToPosition);
// Genre
const MapDataToGenre = {
a: GenreEnum.All,
r: GenreEnum.Rock,
po: GenreEnum.Pop,
i: GenreEnum.Indie,
j: GenreEnum.Jazz,
pu: GenreEnum.Punk,
m: GenreEnum.Metal,
};
const MapGenreToData = _.invert(MapDataToGenre);
// Level
const MapDataToLevel = {
p: LevelEnum.Pro,
a: LevelEnum.Amature,
s: LevelEnum.Study,
h: LevelEnum.Hobby,
};
const MapLevelToData = _.invert(MapDataToLevel);
// Location
const MapDataToLocation = {
e: LocationEnum.Everywhere,
su: LocationEnum.SeoUl,
gn: LocationEnum.GyeonGgiNorth,
gs: LocationEnum.GyeonGgiSouth,
// ... (19 regions)
};
const MapLocationToData = _.invert(MapDataToLocation);const ZodSchema_User = z.object({
accept_marketing: z.boolean(),
nickname: z
.string()
.min(1, { message: "๋๋ค์์ ์
๋ ฅํด์ฃผ์ธ์" })
.max(8, { message: "๋๋ค์์ ์
๋ ฅํด์ฃผ์ธ์" })
.regex(/^[a-zA-Z๊ฐ-ํฃ0-9]+$/, "์ฌ๋ฐ๋ฅธ ๋๋ค์์ ์
๋ ฅํด์ฃผ์ธ์"),
tags: z
.array(z.string())
.refine((arr) => arr.every((item) => /^#[๊ฐ-ํฃa-zA-Z]+$|^$/.test(item)), {
message: "ํ๊ทธ์๋ ์ฌ๋ฐ๋ฅธ ๋ฌธ์ฅ์ ์
๋ ฅํด์ผํด์",
}),
pos: z.nativeEnum(PositionEnum),
genre: z.nativeEnum(GenreEnum),
level: z.nativeEnum(LevelEnum),
location: z.nativeEnum(LocationEnum).optional(),
});const ZodSchema_Post = z.object({
title: z
.string()
.min(5, { message: "์ ๋ชฉ์ 5๊ธ์ ์ด์ ์
๋ ฅํด์ฃผ์ธ์." })
.max(20, { message: "์ ๋ชฉ์ 20๊ธ์๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค." }),
body: z.string().max(1000, { message: "๋ด์ฉ์ 1000๊ธ์๋ฅผ ์ด๊ณผํ ์ ์์ต๋๋ค." }),
location: z.array(z.nativeEnum(LocationEnum)).nonempty(),
genre: z.array(z.nativeEnum(GenreEnum)).nonempty(),
level: z.array(z.nativeEnum(LevelEnum)).nonempty(),
headcnt_by_pos: z.array(ZodSchema_HeadCnt),
datetime: z.string().datetime(),
songs: z.array(ZodSchema_Song),
images: z.array(ZodSchema_Image),
location_detail: ZodSchema_LocationDetail,
openchat: z.string().optional(),
});const ZodSchema_HeadCnt = z.object({
pos: z.nativeEnum(PositionEnum),
headcnt: z.number().min(1),
});
const ZodSchema_Song = z.object({
title: z.string(),
artist: z.string(),
album_cover_url: z.string().optional(),
});
const ZodSchema_LocationDetail = z.object({
name: z.string(),
address: z.string(),
category: z.string().optional(),
});
const ZodSchema_Image = z.object({
url: z.string(),
file: z.any().optional(),
});# Watch mode (during development)
pnpm test
# Run all tests (CI mode)
pnpm test:run
# Generate coverage report
pnpm test:coverage
# UI mode (view tests in the browser)
pnpm test:ui# Run E2E tests
pnpm test:e2e
# UI mode (debugging)
pnpm test:e2e:ui
# Run a specific browser only
pnpm test:e2e --project=chromium__tests__/
โโโ unit/ # Unit tests (185 cases)
โ # - lib/ (pure functions, utils)
โ # - stores/ (Zustand stores)
โ # - hooks/ (Custom hooks)
โโโ integration/ # Integration tests (117 cases)
โ # - auth/ (authentication system)
โ # - api/ (Server Actions)
โ # - external/ (external APIs)
โโโ component/ # Component tests (35 cases)
โ # - React Testing Library
โโโ e2e/ # E2E tests (31 cases)
# - Playwright
Runs automatically via GitHub Actions:
- Unit/integration tests run on PR/push
- E2E tests run on
masterpush
File: .github/workflows/test.yml
- Context - Project context
- Architecture - System structure
- Patterns - Code patterns