Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/features/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { AuthProvider } from './model/AuthProvider'
export { useAuth } from './model/useAuth'
export { useGetStartedTarget } from './model/useGetStartedTarget'
export { useLogout } from './model/useLogout'
export type { AuthContextValue, AuthStatus } from './model/AuthContext'
export { GithubLoginButton } from './ui/GithubLoginButton'
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/features/auth/model/useGetStartedTarget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useAuth } from './useAuth'

// "Get Started" CTA 목적지. 헤더(SiteNav)와 동일한 분기:
// 로그인 상태면 워크스페이스, 아니면 로그인 페이지.
// (loading 중에도 로그인으로 — 로그인 페이지가 인증 사용자를 워크스페이스로 보낸다.)
export function useGetStartedTarget(): string {
const { status } = useAuth()
return status === 'authenticated' ? '/workspace' : '/login'
}
4 changes: 3 additions & 1 deletion frontend/src/features/interview/ui/live/InterviewStage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ export function InterviewStage({
className="absolute inset-0 bg-cover bg-center"
style={{ backgroundImage: `url(${BG})` }}
/>
{/* 배경 사진이 보이도록 옅은 스크림만. 텍스트 가독성은 헤더·질문 카드·
컴포저가 각자 배경(반투명+blur / solid)으로 확보한다. */}
<div
aria-hidden
className="absolute inset-0 bg-gradient-to-b from-white/55 via-white/25 to-white/75"
className="absolute inset-0 bg-gradient-to-b from-white/30 via-white/10 to-white/45"
/>

{/* 진행도 바 */}
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/widgets/home-cta/ui/HomeCta.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Link } from 'react-router-dom'
import { useGetStartedTarget } from '@/features/auth'

export function HomeCta() {
const getStartedTo = useGetStartedTarget()
return (
<section id="cta" className="bg-bg">
<div className="mx-auto max-w-content px-6 lg:px-12 pb-24 lg:pb-32">
Expand Down Expand Up @@ -31,8 +35,8 @@ export function HomeCta() {
지금 GitHub 계정만 연결하면, 30초 안에 첫 모의면접이 시작됩니다.
</p>

<a
href="#login"
<Link
to={getStartedTo}
className="mt-10 inline-flex items-center gap-2 pl-5 pr-2 py-2.5 rounded-pill bg-[#e6dfd4] text-sage-900 text-button hover:bg-white transition-colors duration-fast"
>
Get Started
Expand All @@ -42,7 +46,7 @@ export function HomeCta() {
>
</span>
</a>
</Link>
</div>
</div>
</div>
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/widgets/home-hero/ui/ScreenContent.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Link } from 'react-router-dom'
import { useGetStartedTarget } from '@/features/auth'
import { useTypewriter } from '@/shared/hooks'

const TYPED_TEXT = 'Stack Up'
Expand All @@ -7,6 +9,7 @@ export function ScreenContent() {
startDelayMs: 850,
stepMs: 130,
})
const getStartedTo = useGetStartedTarget()

return (
<div className="relative text-center w-full">
Expand Down Expand Up @@ -42,8 +45,8 @@ export function ScreenContent() {
</h1>

<div className="anim-hero-rise mt-4 sm:mt-6 [animation-delay:2.0s]">
<a
href="#cta"
<Link
to={getStartedTo}
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-pill bg-sage-700 text-white text-button hover:bg-sage-800 transition-colors duration-fast"
>
Get Started
Expand All @@ -53,7 +56,7 @@ export function ScreenContent() {
>
</span>
</a>
</Link>
</div>
</div>
)
Expand Down
136 changes: 94 additions & 42 deletions frontend/src/widgets/home-services/ui/HomeServices.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Link } from 'react-router-dom'
import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Modal } from '@/shared/ui'

type ServiceCard = {
tag: string
title: string
image: string
to: string
}
} & ({ to: string } | { modal: 'role' })

const services: ServiceCard[] = [
{
Expand All @@ -18,7 +19,8 @@ const services: ServiceCard[] = [
tag: 'Free Plan',
title: '직무 기술 면접\nRole-based Interview',
image: '/second-section-backend-interview.avif',
to: '/practice/role',
// 직무는 프론트/백엔드를 모달에서 고른 뒤 해당 연습으로 이동.
modal: 'role',
},
{
tag: 'Free Plan',
Expand All @@ -28,7 +30,61 @@ const services: ServiceCard[] = [
},
]

const ROLE_OPTIONS = [
{ track: 'frontend', label: '프론트엔드', desc: 'HTML·CSS·JS·React·브라우저' },
{ track: 'backend', label: '백엔드', desc: 'OS·네트워크·DB·자바·스프링' },
] as const

const cardClass =
'group relative block rounded-2xl overflow-hidden aspect-[3/4] bg-sage-800 text-left'

function CardInner({ card }: { card: ServiceCard }) {
return (
<>
<img
src={card.image}
alt={card.title}
loading="lazy"
decoding="async"
className="absolute inset-0 w-full h-full object-cover transition-transform duration-slow ease-standard group-hover:scale-[1.04]"
/>
<div
aria-hidden
className="absolute inset-0"
style={{
background:
'linear-gradient(180deg, rgba(20,26,17,0.05) 0%, rgba(20,26,17,0.05) 55%, rgba(20,26,17,0.75) 100%)',
}}
/>

<span className="absolute top-4 left-4 px-3 py-1 rounded-pill bg-[#e6dfd4] text-sage-900 text-caption font-medium">
{card.tag}
</span>

<div className="absolute inset-x-5 bottom-5 flex items-end justify-between gap-4 text-white">
<h3 className="whitespace-pre-line font-heading font-semibold text-[26px] lg:text-[28px] leading-tight text-white">
{card.title}
</h3>
<span
aria-hidden
className="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-pill bg-white/12 backdrop-blur-sm border border-white/20 transition-transform duration-fast group-hover:translate-x-1"
>
</span>
</div>
</>
)
}

export function HomeServices() {
const navigate = useNavigate()
const [roleOpen, setRoleOpen] = useState(false)

const goRole = (track: (typeof ROLE_OPTIONS)[number]['track']) => {
setRoleOpen(false)
navigate(`/practice/${track}`)
}

return (
<section id="services" className="bg-bg">
<div className="mx-auto max-w-content px-6 lg:px-12 pt-16 pb-24 lg:pt-24 lg:pb-32">
Expand All @@ -40,47 +96,43 @@ export function HomeServices() {
</h2>

<div className="mt-12 grid gap-5 md:grid-cols-2 lg:grid-cols-3">
{services.map((s) => (
<Link
key={s.title}
to={s.to}
className="group relative block rounded-2xl overflow-hidden aspect-[3/4] bg-sage-800"
>
<img
src={s.image}
alt={s.title}
loading="lazy"
decoding="async"
className="absolute inset-0 w-full h-full object-cover transition-transform duration-slow ease-standard group-hover:scale-[1.04]"
/>
<div
aria-hidden
className="absolute inset-0"
style={{
background:
'linear-gradient(180deg, rgba(20,26,17,0.05) 0%, rgba(20,26,17,0.05) 55%, rgba(20,26,17,0.75) 100%)',
}}
/>

<span className="absolute top-4 left-4 px-3 py-1 rounded-pill bg-[#e6dfd4] text-sage-900 text-caption font-medium">
{s.tag}
</span>
{services.map((s) =>
'to' in s ? (
<Link key={s.title} to={s.to} className={cardClass}>
<CardInner card={s} />
</Link>
) : (
<button
key={s.title}
type="button"
onClick={() => setRoleOpen(true)}
className={cardClass}
>
<CardInner card={s} />
</button>
),
)}
</div>
</div>

<div className="absolute inset-x-5 bottom-5 flex items-end justify-between gap-4 text-white">
<h3 className="whitespace-pre-line font-heading font-semibold text-[26px] lg:text-[28px] leading-tight text-white">
{s.title}
</h3>
<span
aria-hidden
className="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-pill bg-white/12 backdrop-blur-sm border border-white/20 transition-transform duration-fast group-hover:translate-x-1"
>
</span>
</div>
</Link>
<Modal open={roleOpen} onClose={() => setRoleOpen(false)} title="직무 기술 면접">
<p className="text-body text-fg-muted">
연습할 직무를 선택하면 해당 분야 질문이 무작위로 출제됩니다.
</p>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{ROLE_OPTIONS.map((opt) => (
<button
key={opt.track}
type="button"
onClick={() => goRole(opt.track)}
className="flex flex-col gap-1 rounded-lg border border-border bg-surface-raised px-4 py-4 text-left transition-colors hover:border-border-strong hover:bg-surface"
>
<span className="text-button text-fg">{opt.label}</span>
<span className="text-caption text-fg-muted">{opt.desc}</span>
</button>
))}
</div>
</div>
</Modal>
</section>
)
}
4 changes: 3 additions & 1 deletion frontend/src/widgets/site-footer/ui/SiteFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Link } from 'react-router-dom'
import { useGetStartedTarget } from '@/features/auth'

// 단순 뷰 섹션 widgets 에선 굳이 나누지 않는게 좋다고 판단했습니다.
// 상수, 메세지 등 마찬가지
Expand Down Expand Up @@ -33,6 +34,7 @@ const columns: { title: string; links: FooterLink[] }[] = [
]

export function SiteFooter() {
const getStartedTo = useGetStartedTarget()
return (
<footer
id="footer"
Expand All @@ -48,7 +50,7 @@ export function SiteFooter() {
One smart step
</h2>
<Link
to="/#cta"
to={getStartedTo}
className="inline-flex self-start lg:self-end items-center gap-2 pl-5 pr-2 py-2.5 rounded-pill bg-[#e6dfd4] text-sage-900 text-button hover:bg-white transition-colors duration-fast"
>
Get Started
Expand Down