[fix] solution/frontend: 로그인해도 랜딩·요금에 갈 길을 낸다 — '/' 관문 제거

로그인하면 '/' 가 무조건 /sites 로 튕겼다. 그래서 **로고를 눌러도 랜딩이 안 뜨고**,
/pricing 은 살아 있는데 링크가 랜딩과 MarketingShell 에만 있어 로그인한 사장님은
주소를 직접 쳐야 했다. 요금은 쓰는 도중에 확인하는 값이지 가입 전에만 보는 값이 아니다.

- app/router.tsx: Home 의 리다이렉트 제거. '/' 는 누구에게나 랜딩이다.
  "로그인 직후엔 내 사이트로" 는 로그인·가입 화면이 직접 보낸다.
- pages/LoginPage.tsx: 도착지를 `homePath` prop 으로 받는다. 기본값 '/' 라
  내부 운영 앱(자기 '/' 가 사업장 목록으로 간다)은 그대로다. 사장님 앱만 '/sites'.
- pages/SignupPage.tsx: 가입 후 '/' → '/sites'. 안 그러면 관문이 없어진 지금 랜딩에 떨어진다.
- 로그인·가입 화면 로고에 '/' 링크. 그 화면에서 빠져나갈 길이 하나도 없었다.
- layout/AppShell.tsx: 로그아웃 → '/login' 이 아니라 '/'. 나간 사람에게 로그인 폼을
  다시 들이밀지 않는다. 사이드바에 [요금] 추가.
- layout/MarketingShell.tsx: 헤더 [무료로 만들기] 제거 — 히어로 입력 카드가 이미 그 자리다.
  시작하는 문이 한 화면에 둘이면 어느 쪽이 진짜인지 고르게 만든다.

검증: tsc·eslint 통과. docker compose up -d --build solution-site 로 띄워 눌러 확인 —
로그인 상태에서 로고 → 랜딩, 헤더는 [이렇게 나옵니다 · 요금 · 내 사이트].
This commit is contained in:
Mina Choi 2026-09-04 17:29:13 +09:00
parent 238d4c25c4
commit 7a6fee70c1
5 changed files with 59 additions and 32 deletions

View File

@ -32,12 +32,18 @@ function Home() {
</div> </div>
); );
} }
if (user) return <Navigate to="/sites" replace />; /*
* ★ 로그인해도 랜딩을 보여 준다 (2026-09-04, 사장님 지적)
* 예전엔 여기서 /sites 로 튕겼다. 그러면 **로그인한 사장님은 랜딩·요금에 갈 길이 없다** —
* 로고가 '/' 를 걸고 있어도 눌리는 순간 목록으로 돌아온다.
* "로그인 직후엔 내 사이트로" 는 로그인·가입 화면이 직접 보낸다(LoginPage homePath).
*/
void user;
return <LandingPage />; return <LandingPage />;
} }
export const router = createBrowserRouter([ export const router = createBrowserRouter([
{path: '/login', element: <LoginPage />}, {path: '/login', element: <LoginPage homePath="/sites" />},
// 로그인 화면의 [회원가입] 이 여기로 온다. 이 줄이 없으면 링크는 있고 목적지만 404 다. // 로그인 화면의 [회원가입] 이 여기로 온다. 이 줄이 없으면 링크는 있고 목적지만 404 다.
{path: '/signup', element: <SignupPage />}, {path: '/signup', element: <SignupPage />},

View File

@ -1,6 +1,6 @@
import type {ComponentType, ReactNode} from 'react'; import type {ComponentType, ReactNode} from 'react';
import {Link, NavLink, useLocation, useNavigate} from 'react-router'; import {Link, NavLink, useLocation, useNavigate} from 'react-router';
import {LayoutGrid, LogIn, LogOut, Search, Store, Wand2} from 'lucide-react'; import {LayoutGrid, LogIn, LogOut, Receipt, Search, Store, Wand2} from 'lucide-react';
import {cn} from '@/lib/utils'; import {cn} from '@/lib/utils';
import {userLabel, useAuthStore} from '@/stores/auth'; import {userLabel, useAuthStore} from '@/stores/auth';
@ -25,6 +25,15 @@ export type NavItem = {
const OWNER_NAV: NavItem[] = [ const OWNER_NAV: NavItem[] = [
{to: '/sites', match: '/sites', label: '내 사이트', icon: Store}, {to: '/sites', match: '/sites', label: '내 사이트', icon: Store},
{to: '/builder?new=1', match: '/builder', label: '새 사이트', icon: Wand2}, {to: '/builder?new=1', match: '/builder', label: '새 사이트', icon: Wand2},
/*
* ★ 요금은 **로그인한 뒤에도** 갈 길이 있어야 한다 (2026-09-04, 사장님 지적)
* /pricing 은 살아 있는데 링크가 MarketingShell 과 랜딩에만 있었다. 둘 다 로그인하면
* 안 보이는 화면이라(로그인하면 / 가 /sites 로 간다), 사장님은 주소를 직접 쳐야 했다.
* 요금은 쓰는 도중에 확인하는 값이지 가입 전에만 보는 값이 아니다.
* ★ 목적지가 MarketingShell(사이드바 없는 문서형)이라 사이드바가 사라진다. 그래도 갇히지
* 않는다 — 저쪽 헤더가 로그인 상태를 알아보고 [내 사이트] 버튼을 세운다.
*/
{to: '/pricing', match: '/pricing', label: '요금', icon: Receipt},
]; ];
export function AppShell({children, nav = OWNER_NAV}: {children: ReactNode; nav?: NavItem[]}) { export function AppShell({children, nav = OWNER_NAV}: {children: ReactNode; nav?: NavItem[]}) {
@ -87,7 +96,8 @@ export function AppShell({children, nav = OWNER_NAV}: {children: ReactNode; nav?
// 눌러도 아무 일이 없는 것처럼 보인다 — 로그인 화면으로 보낸다. // 눌러도 아무 일이 없는 것처럼 보인다 — 로그인 화면으로 보낸다.
onClick={() => { onClick={() => {
signOut(); signOut();
navigate('/login'); // ★ 로그인 화면이 아니라 랜딩으로. 나간 사람에게 다시 로그인 폼을 들이밀지 않는다.
navigate('/');
}} }}
className="flex w-full cursor-pointer items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-sidebar-foreground transition-colors hover:bg-sidebar-accent/60" className="flex w-full cursor-pointer items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-sidebar-foreground transition-colors hover:bg-sidebar-accent/60"
> >

View File

@ -47,6 +47,11 @@ export function MarketingShell({children}: {children: ReactNode}) {
<div className="ml-auto flex items-center gap-3"> <div className="ml-auto flex items-center gap-3">
{/* ★ 이미 사이트를 가진 사장님에게 [로그인] 을 다시 보여주지 않는다 — 갈 곳은 내 사이트다. */} {/* ★ 이미 사이트를 가진 사장님에게 [로그인] 을 다시 보여주지 않는다 — 갈 곳은 내 사이트다. */}
{/*
★ [무료로 만들기] 를 뺐다 (2026-09-04, 사장님 지시)
히어로의 큰 입력 카드가 이미 그 자리다 — 시작하는 문이 한 화면에 둘이면
어느 쪽이 진짜인지 고르게 만든다. 헤더는 로그인만 받는다.
*/}
{user ? ( {user ? (
<Link <Link
to="/sites" to="/sites"
@ -55,21 +60,12 @@ export function MarketingShell({children}: {children: ReactNode}) {
내 사이트 내 사이트
</Link> </Link>
) : ( ) : (
<>
{/* 아임웹처럼 둘 다 버튼이다 — 로그인만 맨 텍스트면 눌리는 것으로 안 보인다. */}
<Link <Link
to="/login" to="/login"
className="rounded-md border border-border px-4 py-2 text-[13px] font-medium transition-colors hover:bg-muted" className="rounded-md border border-border px-4 py-2 text-[13px] font-medium transition-colors hover:bg-muted"
> >
로그인 로그인
</Link> </Link>
<Link
to="/builder?new=1"
className="rounded-md bg-primary px-4 py-2 text-[13px] font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
무료로 만들기
</Link>
</>
)} )}
</div> </div>
</div> </div>

View File

@ -17,7 +17,21 @@ import {useAuthStore} from '@/stores/auth';
* 우리가 만들어 준다. 가입 링크를 두 곳에 다 두면 admin 라우터에 없는 `/signup` 으로 보내 * 우리가 만들어 준다. 가입 링크를 두 곳에 다 두면 admin 라우터에 없는 `/signup` 으로 보내
* 404 가 난다(admin/src/app/router.tsx 에 그 경로는 없다). * 404 가 난다(admin/src/app/router.tsx 에 그 경로는 없다).
*/ */
export function LoginPage({selfServe = true}: {selfServe?: boolean}) { export function LoginPage({
selfServe = true,
homePath = '/',
}: {
selfServe?: boolean;
/**
* 로그인 직후 도착지.
*
* ★ 예전엔 '/' 로 보내고 각 앱의 '/' 리다이렉트가 알아서 가르게 했다. 그런데 사장님 앱에서
* 그 관문이 **로그인한 사람에게 랜딩을 영영 못 보게** 만들었다 — 로고를 눌러도 '/' 가
* /sites 로 튕긴다(사장님 지적). 관문을 걷고 도착지를 여기서 받는다.
* ★ 기본값은 '/' 다. 내부 운영 앱은 자기 '/' 가 사업장 목록으로 보내므로 그대로 맞는다.
*/
homePath?: string;
}) {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const user = useAuthStore((s) => s.user); const user = useAuthStore((s) => s.user);
@ -28,9 +42,8 @@ export function LoginPage({selfServe = true}: {selfServe?: boolean}) {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// ★ 기본 도착지를 '/' 로 둔다. 앱마다 홈이 다르고(사장님 → 빌더, 내부 → 사업장 목록) // 원래 가려던 곳이 있으면 그리로, 없으면 앱이 준 홈으로.
// 각 라우터의 '/' 리다이렉트가 이미 그걸 안다. 여기 경로를 박으면 한쪽에서 404 다. const from = (location.state as {from?: string} | null)?.from ?? homePath;
const from = (location.state as {from?: string} | null)?.from ?? '/';
if (user) return <Navigate to={from} replace />; if (user) return <Navigate to={from} replace />;
@ -82,11 +95,10 @@ export function LoginPage({selfServe = true}: {selfServe?: boolean}) {
className="w-full max-w-sm space-y-4 rounded-2xl border border-border bg-card p-7" className="w-full max-w-sm space-y-4 rounded-2xl border border-border bg-card p-7"
> >
<div className="space-y-1 text-center"> <div className="space-y-1 text-center">
<img {/* 로고는 어디서든 랜딩으로 돌아가는 문이다. 로그인 화면에서 나갈 길이 여기뿐이었다. */}
src="/brand/web4ai-wordmark.svg" <Link to="/" className="mx-auto mb-3 block w-fit">
alt="Web4Ai" <img src="/brand/web4ai-wordmark.svg" alt="Web4Ai" className="h-9 w-auto" />
className="mx-auto mb-3 h-9 w-auto" </Link>
/>
<h1 className="text-sm font-medium text-muted-foreground"> <h1 className="text-sm font-medium text-muted-foreground">
{selfServe ? '로그인' : '관리자'} {selfServe ? '로그인' : '관리자'}
</h1> </h1>

View File

@ -35,7 +35,7 @@ export function SignupPage() {
}); });
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
if (user) return <Navigate to="/" replace />; if (user) return <Navigate to="/sites" replace />;
const set = (key: keyof typeof form) => (e: {target: {value: string}}) => const set = (key: keyof typeof form) => (e: {target: {value: string}}) =>
setForm((prev) => ({...prev, [key]: e.target.value})); setForm((prev) => ({...prev, [key]: e.target.value}));
@ -76,7 +76,7 @@ export function SignupPage() {
return; return;
} }
notify.success('가입이 완료되었습니다.'); notify.success('가입이 완료되었습니다.');
navigate('/', {replace: true}); navigate('/sites', {replace: true});
} catch (error) { } catch (error) {
notifyApiError(error, '가입하지 못했습니다.'); notifyApiError(error, '가입하지 못했습니다.');
} finally { } finally {
@ -97,7 +97,7 @@ export function SignupPage() {
notifyApiError({data: res}, '로그인 응답에 토큰이 없습니다.'); notifyApiError({data: res}, '로그인 응답에 토큰이 없습니다.');
return; return;
} }
navigate('/', {replace: true}); navigate('/sites', {replace: true});
} catch (error) { } catch (error) {
notifyApiError(error, '구글로 가입하지 못했습니다.'); notifyApiError(error, '구글로 가입하지 못했습니다.');
} finally { } finally {
@ -112,7 +112,10 @@ export function SignupPage() {
className="w-full max-w-sm space-y-4 rounded-2xl border border-border bg-card p-7" className="w-full max-w-sm space-y-4 rounded-2xl border border-border bg-card p-7"
> >
<div className="space-y-1 text-center"> <div className="space-y-1 text-center">
<img src="/brand/web4ai-wordmark.svg" alt="Web4Ai" className="mx-auto mb-3 h-9 w-auto" /> {/* 로고는 어디서든 랜딩으로 돌아가는 문이다. */}
<Link to="/" className="mx-auto mb-3 block w-fit">
<img src="/brand/web4ai-wordmark.svg" alt="Web4Ai" className="h-9 w-auto" />
</Link>
<h1 className="text-sm font-medium text-muted-foreground">회원가입</h1> <h1 className="text-sm font-medium text-muted-foreground">회원가입</h1>
<p className="text-xs text-muted-foreground">내 가게 사이트를 만들려면 계정이 필요합니다.</p> <p className="text-xs text-muted-foreground">내 가게 사이트를 만들려면 계정이 필요합니다.</p>
</div> </div>