add icon for user
This commit is contained in:
parent
ef08b9f433
commit
78cf43d7ff
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 ""))
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user