Skip to content

Latest commit

ย 

History

History
644 lines (530 loc) ยท 14.7 KB

File metadata and controls

644 lines (530 loc) ยท 14.7 KB

Reference

API, database schema, and type definition reference

Database Schema

user table

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[];
}

jamsession table

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[];
}

jamentry table

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;
}

playlist table

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.)

user_nickname table

Table used for nickname uniqueness checks.

Column Type Description
nickname string PK, nickname

API Endpoints

/api/auth/callback (GET)

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

/api/maniadb (POST)

ManiaDB song search

Request:

{
  "songName": "๊ฒ€์ƒ‰์–ด"
}

Response:

{
  "status": 200,
  "msg": "Success",
  "data": [
    {
      "title": "๊ณก๋ช…",
      "artist": "์•„ํ‹ฐ์ŠคํŠธ",
      "album_cover_url": "URL"
    }
  ]
}

File: app/api/maniadb/route.ts

/api/naver/map (POST)

Naver Local Search API

Request:

{
  "query": "์žฅ์†Œ ๊ฒ€์ƒ‰์–ด"
}

Response:

{
  "status": 200,
  "msg": "Success",
  "data": [
    {
      "name": "์žฅ์†Œ๋ช…",
      "address": "์ฃผ์†Œ",
      "category": "์นดํ…Œ๊ณ ๋ฆฌ"
    }
  ]
}

File: app/api/naver/map/route.ts


Server Actions

Create (app/api/functions/create.ts)

Function Parameters Returns Description
createJamEntry CreateJamEntryDto Promise<boolean> Create an application

Read (app/api/functions/read.ts)

Function Parameters Returns Description
getUser supabase Promise<User> Retrieve the current user
getJam id: number Promise<Entity_JamSession> Retrieve jam session details

Delete (app/api/functions/delete.ts)

Function Parameters Returns Description
deleteJamSession jam_id: number Promise<boolean> Delete a jam session
deleteJamEntry entryId: number Promise<boolean> Delete an application

Validation (app/api/functions/validation.ts)

Function Parameters Returns Description
isNicknameUnique nickname: string Promise<boolean> Check nickname uniqueness

Fetch (app/lib/functions/fetch.ts)

Function Parameters Returns Description
fetchJams { pageParam, queryFnParam } Promise<{ data, nextPage }> Jam session list (pagination)

JamSession Actions (app/(pages)/(only-user)/jamsession/functions/fetch.ts)

Function Description
createJamSession Create a jam session (including image upload)
updateJamSession Update a jam session
initialize Initialize the session detail page (permission check)

MyAct Actions (app/(pages)/(only-user)/myact/[id]/functions/fetch.ts)

Function Description
allowEntry Approve an application
refuseEntry Reject an application
setNoShow Mark as no-show
letOutMember Remove a participant

TypeScript Types

Enums

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",
}

DTOs (app/api/dtos/create.ts)

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;
}

API Response (app/api/types/response.interface.ts)

interface FetchResult {
  status: number;   // 200: success, 500: failure
  msg: string;      // Status message
  data: any;        // Response data
}

Global Context (app/lib/interfaces/global.interface.ts)

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;
}

Enum-String Mapping (app/lib/maps/jam.ts)

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);

Zod Validation (app/lib/zod.ts)

User Schema

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(),
});

Post Schema

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(),
});

Sub Schemas

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(),
});

Testing Commands

Vitest (unit/integration tests)

# 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

Playwright (E2E tests)

# Run E2E tests
pnpm test:e2e

# UI mode (debugging)
pnpm test:e2e:ui

# Run a specific browser only
pnpm test:e2e --project=chromium

Test Structure

__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

CI/CD

Runs automatically via GitHub Actions:

  • Unit/integration tests run on PR/push
  • E2E tests run on master push

File: .github/workflows/test.yml


Related Documents