feat: add user icon library and in-game selection

This commit is contained in:
2026-08-01 03:40:36 +00:00
parent 395b60cdbe
commit a275e6a234
26 changed files with 1214 additions and 69 deletions
+22
View File
@@ -32,6 +32,14 @@ export interface GatewayUserInfo {
canUseGeneralPicture?: boolean;
createdAt?: string;
legacyMemberNo?: number;
icons?: GatewayUserIconInfo[];
}
export interface GatewayUserIconInfo {
id: string;
picture: string;
imageServer: number;
createdAt: string;
}
export interface GameSessionTokenPayload {
@@ -98,6 +106,20 @@ export const parseGameSessionTokenPayload = (value: unknown): GameSessionTokenPa
(user.profileIconResetAt !== undefined &&
(typeof user.profileIconResetAt !== 'string' || !isCanonicalIsoTimestamp(user.profileIconResetAt))) ||
(user.canUseGeneralPicture !== undefined && typeof user.canUseGeneralPicture !== 'boolean') ||
(user.icons !== undefined &&
(!Array.isArray(user.icons) ||
user.icons.length > 5 ||
user.icons.some(
(icon) =>
!icon ||
typeof icon !== 'object' ||
typeof icon.id !== 'string' ||
typeof icon.picture !== 'string' ||
!Number.isSafeInteger(icon.imageServer) ||
icon.imageServer < 0 ||
typeof icon.createdAt !== 'string' ||
!isCanonicalIsoTimestamp(icon.createdAt)
))) ||
(user.legacyMemberNo !== undefined && (!Number.isSafeInteger(user.legacyMemberNo) || user.legacyMemberNo <= 0))
) {
return null;
+6 -1
View File
@@ -212,6 +212,7 @@ export type TurnDaemonCommand =
picture: string;
imageServer: number;
iconRevision: string;
enforceCooldown?: boolean;
}
| {
type: 'joinCreateGeneral';
@@ -254,6 +255,9 @@ export type TurnDaemonCommand =
uniqueName: string;
personality: string;
seedOwnerIdentity: string | number;
ownerPicture?: string;
ownerImageServer?: number;
ownerIconRevision?: string;
}
| {
type: 'selectPoolReselect';
@@ -534,8 +538,9 @@ export type TurnDaemonCommandResult =
| {
type: 'adjustGeneralIcon';
ok: false;
code: 'CONFLICT' | 'PRECONDITION_FAILED';
code: 'CONFLICT' | 'PRECONDITION_FAILED' | 'TOO_MANY_REQUESTS';
reason: string;
availableAt?: string;
}
| {
type: 'joinCreateGeneral';
@@ -0,0 +1,29 @@
ALTER TABLE "app_user"
ADD COLUMN "icon_retired_at" TIMESTAMP(3);
CREATE TABLE "user_icon" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"user_id" TEXT NOT NULL,
"picture" TEXT NOT NULL,
"image_server" INTEGER NOT NULL DEFAULT 1,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"retired_at" TIMESTAMP(3),
CONSTRAINT "user_icon_pkey" PRIMARY KEY ("id"),
CONSTRAINT "user_icon_user_id_fkey" FOREIGN KEY ("user_id")
REFERENCES "app_user"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "user_icon_picture_key" ON "user_icon"("picture");
CREATE INDEX "user_icon_user_id_retired_at_created_at_idx"
ON "user_icon"("user_id", "retired_at", "created_at");
-- 기존 전콘의 파일명과 URL을 바꾸지 않고 활성 목록의 첫 항목으로 올립니다.
INSERT INTO "user_icon" ("user_id", "picture", "image_server", "created_at")
SELECT
"id",
"picture",
"image_server",
COALESCE("icon_updated_at", "created_at")
FROM "app_user"
WHERE "picture" <> 'default.jpg' AND "image_server" > 0
ON CONFLICT ("picture") DO NOTHING;
+41 -26
View File
@@ -59,36 +59,51 @@ enum GatewaySourceMode {
}
model AppUser {
id String @id @default(uuid())
loginId String @unique @map("login_id")
displayName String @unique @map("display_name")
passwordHash String @map("password_hash")
passwordSalt String @map("password_salt")
roles Json @default(dbgenerated("'[]'::jsonb"))
sanctions Json @default(dbgenerated("'{}'::jsonb"))
oauthType OAuthType @default(NONE) @map("oauth_type")
oauthId String? @unique @map("oauth_id")
email String? @unique
oauthInfo Json @default(dbgenerated("'{}'::jsonb")) @map("oauth_info")
picture String @default("default.jpg")
imageServer Int @default(0) @map("image_server")
iconUpdatedAt DateTime? @map("icon_updated_at")
iconRevision DateTime? @map("icon_revision")
profileIconResetAt DateTime? @map("profile_icon_reset_at")
thirdPartyUse Boolean @default(true) @map("third_party_use")
termsAcceptedAt DateTime? @map("terms_accepted_at")
privacyAcceptedAt DateTime? @map("privacy_accepted_at")
kakaoVerifiedAt DateTime? @map("kakao_verified_at")
kakaoGraceStartedAt DateTime @default(now()) @map("kakao_grace_started_at")
deleteAfter DateTime? @map("delete_after")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
lastLoginAt DateTime? @map("last_login_at")
legacyData Json @default(dbgenerated("'{}'::jsonb")) @map("legacy_data")
id String @id @default(uuid())
loginId String @unique @map("login_id")
displayName String @unique @map("display_name")
passwordHash String @map("password_hash")
passwordSalt String @map("password_salt")
roles Json @default(dbgenerated("'[]'::jsonb"))
sanctions Json @default(dbgenerated("'{}'::jsonb"))
oauthType OAuthType @default(NONE) @map("oauth_type")
oauthId String? @unique @map("oauth_id")
email String? @unique
oauthInfo Json @default(dbgenerated("'{}'::jsonb")) @map("oauth_info")
picture String @default("default.jpg")
imageServer Int @default(0) @map("image_server")
iconUpdatedAt DateTime? @map("icon_updated_at")
iconRevision DateTime? @map("icon_revision")
profileIconResetAt DateTime? @map("profile_icon_reset_at")
iconRetiredAt DateTime? @map("icon_retired_at")
thirdPartyUse Boolean @default(true) @map("third_party_use")
termsAcceptedAt DateTime? @map("terms_accepted_at")
privacyAcceptedAt DateTime? @map("privacy_accepted_at")
kakaoVerifiedAt DateTime? @map("kakao_verified_at")
kakaoGraceStartedAt DateTime @default(now()) @map("kakao_grace_started_at")
deleteAfter DateTime? @map("delete_after")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
lastLoginAt DateTime? @map("last_login_at")
legacyData Json @default(dbgenerated("'{}'::jsonb")) @map("legacy_data")
icons UserIcon[]
@@map("app_user")
}
model UserIcon {
id String @id @default(uuid())
userId String @map("user_id")
user AppUser @relation(fields: [userId], references: [id], onDelete: Cascade)
picture String @unique
imageServer Int @default(1) @map("image_server")
createdAt DateTime @default(now()) @map("created_at")
retiredAt DateTime? @map("retired_at")
@@index([userId, retiredAt, createdAt])
@@map("user_icon")
}
model LegacyMemberLog {
id BigInt @id
memberNo Int @map("member_no")