add icon for user

This commit is contained in:
jaehwang 2026-09-16 13:04:49 +09:00
parent ef08b9f433
commit 78cf43d7ff
7 changed files with 72 additions and 36 deletions

View File

@ -24,6 +24,7 @@ def user_view(user: User) -> dict:
return {
"email": user.email,
"name": user.name,
"picture": user.picture_url,
"jobs_created": user.jobs_created,
"job_limit": user.job_limit,
"jobs_left": user.jobs_left,
@ -33,11 +34,13 @@ def user_view(user: User) -> dict:
async def find_or_create(session: AsyncSession, account) -> User:
user = await session.get(User, account.sub)
if user is None:
user = User(id=account.sub, email=account.email, name=account.name)
user = User(id=account.sub, email=account.email, name=account.name,
picture_url=account.picture)
session.add(user)
else:
# 이메일·이름은 구글 쪽에서 바뀔 수 있어 로그인할 때마다 맞춘다
user.email, user.name = account.email, account.name
# 이메일·이름·사진은 구글 쪽에서 바뀔 수 있어 로그인할 때마다 맞춘다
user.email, user.name, user.picture_url = (account.email, account.name,
account.picture)
await session.commit()
return user

View File

@ -12,6 +12,7 @@ from utils.database import Base
USER_ID_LENGTH = 64
EMAIL_LENGTH = 320
PICTURE_URL_LENGTH = 512
DEFAULT_JOB_LIMIT = 3
# 소유자를 모르는 잡의 자리. 나중에 admin의 sub로 덮어쓴다
@ -24,6 +25,8 @@ class User(Base):
id: Mapped[str] = mapped_column(String(USER_ID_LENGTH), primary_key=True)
email: Mapped[str] = mapped_column(String(EMAIL_LENGTH), index=True)
name: Mapped[str] = mapped_column(String(255), default="")
# 구글 프로필 사진. 토큰에만 들어오는 값이라 로그인할 때 받아 둔다
picture_url: Mapped[str] = mapped_column(String(PICTURE_URL_LENGTH), default="")
# 만들 수 있는 총 잡 수. 무제한으로 둘 계정은 이 값을 크게 올린다
job_limit: Mapped[int] = mapped_column(Integer, default=DEFAULT_JOB_LIMIT)

View File

@ -38,10 +38,11 @@ class GoogleTokenInvalid(RuntimeError):
class GoogleAccount:
def __init__(self, sub: str, email: str, name: str):
def __init__(self, sub: str, email: str, name: str, picture: str):
self.sub = sub
self.email = email
self.name = name
self.picture = picture
def is_enabled() -> bool:
@ -113,4 +114,5 @@ async def verify_id_token(credential: str) -> GoogleAccount:
raise GoogleTokenInvalid("이메일이 인증되지 않은 계정입니다")
return GoogleAccount(sub=sub, email=str(claims.get("email") or ""),
name=str(claims.get("name") or ""))
name=str(claims.get("name") or ""),
picture=str(claims.get("picture") or ""))

View File

@ -20,13 +20,13 @@ export default function Ado2Layout({ children }: { children: React.ReactNode })
<Ado2Logo height={20} />
<span className="sidebar-product">MOVING POSTER</span>
</Link>
<AccountBadge />
</div>
<nav className="sidebar-menu">
<NavLink href="/" icon="video"></NavLink>
{FEATURES.styling && <NavLink href="/studio" icon="image"> </NavLink>}
{FEATURES.archive && <NavLink href="/archive" icon="folder"></NavLink>}
</nav>
<AccountBadge />
<div className="sidebar-foot"> · </div>
</aside>
<main className="main-content">

View File

@ -104,30 +104,48 @@ body { margin: 0; -webkit-font-smoothing: antialiased; letter-spacing: -0.006em;
}
.sidebar-logo { padding: 1.5rem 1.5rem 0.5rem; }
/* 로고 아래 로그인 상태 */
.account-badge {
/* 사이드바 맨 아래 계정 카드 */
.account-card {
display: flex;
align-items: baseline;
gap: 0.5rem;
margin-top: 0.6rem;
align-items: center;
gap: 0.6rem;
margin: 0 0.75rem;
padding: 0.7rem 0.8rem;
border-radius: var(--radius-md);
background: var(--color-bg-darker);
border: 1px solid var(--color-border-white-10);
min-width: 0;
}
.account-badge-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--text-xs);
color: var(--color-text-gray-400);
}
.account-badge-quota {
.account-photo {
flex: none;
font-size: var(--text-xs);
width: 34px;
height: 34px;
border-radius: var(--radius-full);
object-fit: cover;
background: var(--color-bg-dark);
}
.account-photo--blank {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: var(--color-mint);
}
.account-badge-link {
.account-body { flex: 1; min-width: 0; }
.account-name {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--text-sm);
font-weight: 700;
}
.account-credit {
margin: 2px 0 0;
font-size: var(--text-xs);
color: var(--color-text-gray-400);
}
.account-link {
flex: none;
padding: 0;
border: none;
@ -137,7 +155,7 @@ body { margin: 0; -webkit-font-smoothing: antialiased; letter-spacing: -0.006em;
font-size: var(--text-xs);
color: var(--color-mint);
}
.account-badge-link:hover { text-decoration: underline; }
.account-link:hover { text-decoration: underline; }
.sidebar-menu { flex: 1; padding: 0 0.75rem; margin-top: 1rem; overflow-y: auto; }
.sidebar-item {
display: flex;

View File

@ -1,6 +1,6 @@
"use client";
/** 사이드바 로고 아래에 붙는 로그인 상태. 누구로 들어와 있는지와 나가는 길 */
/** 사이드바 맨 아래 계정 카드. 누구로 들어와 있는지, 남은 크레딧, 나가는 길 */
import { useRouter } from "next/navigation";
import { signOut, useMe } from "@/lib/auth";
@ -13,22 +13,31 @@ export default function AccountBadge() {
if (me === false) {
return (
<button type="button" className="account-badge-link"
onClick={() => router.push("/login")}>
</button>
<div className="account-card">
<button type="button" className="account-link" onClick={() => router.push("/login")}>
</button>
</div>
);
}
return (
<div className="account-badge">
<span className="account-badge-name" title={me.email}>{me.name || me.email}</span>
<span className="account-badge-quota" title="남은 크레딧">
{me.jobs_left}/{me.job_limit}
</span>
<button type="button" className="account-badge-link"
<div className="account-card">
{me.picture ? (
// eslint-disable-next-line @next/next/no-img-element
<img className="account-photo" src={me.picture} alt="" referrerPolicy="no-referrer" />
) : (
<span className="account-photo account-photo--blank">
{(me.name || me.email).slice(0, 1)}
</span>
)}
<div className="account-body">
<p className="account-name" title={me.email}>{me.name || me.email}</p>
<p className="account-credit"> : {me.jobs_left}</p>
</div>
<button type="button" className="account-link"
onClick={() => { signOut().then(() => setMe(false)).then(() => router.push("/")); }}>
</button>
</div>
);

View File

@ -17,6 +17,7 @@ const SCRIPT_ID = "google-identity-services";
export interface Me {
email: string;
name: string;
picture: string;
jobs_created: number;
job_limit: number;
jobs_left: number;