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
7 changes: 7 additions & 0 deletions apps/web/public/icons/google.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/web/public/icons/kakao.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions apps/web/src/views/login/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
interface ButtonProps {
label: string;
style?: React.CSSProperties;
onClick?: () => void;
disabled?: boolean;
className?: string;
}

export function Button({ label, style, onClick, disabled, className }: ButtonProps) {
return (
<button
type="button"
style={{
backgroundColor: disabled ? "#B7B7B7" : "#3A8DFF",
color: "white",
borderRadius: "5px",
height: "50px",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: disabled ? "not-allowed" : "pointer",
position: "relative",
width: "100%",
...style,
}}
Comment on lines +11 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

border와 outline 속성을 추가하세요.

button 요소의 기본 border를 제거하지 않으면 브라우저마다 다른 스타일이 적용될 수 있습니다.

다음 diff를 적용하세요:

       style={{
         backgroundColor: disabled ? "#B7B7B7" : "#3A8DFF",
         color: "white",
+        border: "none",
+        outline: "none",
         borderRadius: "5px",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
type="button"
style={{
backgroundColor: disabled ? "#B7B7B7" : "#3A8DFF",
color: "white",
borderRadius: "5px",
height: "50px",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: disabled ? "not-allowed" : "pointer",
position: "relative",
width: "100%",
...style,
}}
<button
type="button"
style={{
backgroundColor: disabled ? "#B7B7B7" : "#3A8DFF",
color: "white",
border: "none",
outline: "none",
borderRadius: "5px",
height: "50px",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: disabled ? "not-allowed" : "pointer",
position: "relative",
width: "100%",
...style,
}}
🤖 Prompt for AI Agents
In apps/web/src/views/login/components/Button.tsx around lines 11-25, the inline
style for the button is missing explicit border and outline properties; add
border: "none" and outline: "none" to the style object (keeping the existing
conditional and spread), ensuring the button removes browser-default borders and
outlines; if desired, also add a visible focus style separately for
accessibility instead of removing focus outlines entirely.

onClick={onClick}
disabled={disabled}
className={className}
>
<span style={{ fontSize: 16, fontWeight: 600, letterSpacing: "-0.32px" }}>{label}</span>
</button>
);
Comment thread
seungdeok marked this conversation as resolved.
}
48 changes: 48 additions & 0 deletions apps/web/src/views/login/components/SNSLoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import Image from "next/image";

export function SNSLoginButton({ provider }: { provider: "google" | "kakao" }) {
const variants = {
google: {
label: "구글 로그인",
bg: "#FFFFFF",
color: "#000000",
border: "1px solid #E3E8EF",
},
kakao: {
label: "카카오 로그인",
bg: "#FEE500",
color: "#000000",
border: "1px solid #FEE500",
},
};

return (
<button
type="button"
style={{
background: variants[provider].bg,
color: variants[provider].color,
border: variants[provider].border,
borderRadius: "5px",
height: "45px",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
position: "relative",
width: "100%",
}}
>
<Image
src={`/icons/${provider}.svg`}
alt={variants[provider].label}
width={26}
height={26}
style={{ position: "absolute", left: "25px" }}
/>
Comment thread
seungdeok marked this conversation as resolved.
<span style={{ fontSize: 16, fontWeight: 600, letterSpacing: "-0.32px" }}>{variants[provider].label}</span>
</button>
Comment thread
seungdeok marked this conversation as resolved.
);
}
107 changes: 107 additions & 0 deletions apps/web/src/views/login/components/TextField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"use client";

import { useId, useState } from "react";

interface TextFieldProps {
type: "text" | "password";
label: string;
validate?: (value: string) => boolean;
onChange?: (value: string) => void;
value?: string;
className?: string;
style?: React.CSSProperties;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
placeholder?: string;
error?: string;
}

export function TextField({
type,
label,
validate,
onChange,
value,
className,
style,
required,
disabled,
readonly,
placeholder,
error,
}: TextFieldProps) {
const id = useId();
const [isFocused, setIsFocused] = useState(false);
const hasValidationError = validate ? !validate(value || "") : false;
const displayError = hasValidationError ? error : "";
Comment thread
seungdeok marked this conversation as resolved.

const getBorderColor = () => {
if (hasValidationError) return "#E52929"; // 에러 상태
if (isFocused) return "#3A8DFF"; // focus 상태
return "#E3E8EF";
};

return (
<div style={{ position: "relative" }}>
<label
style={{
color: "#111",
fontSize: "12px",
fontStyle: "normal",
fontWeight: "600",
lineHeight: "140%",
letterSpacing: "-0.24px",
marginBottom: "2px",
}}
htmlFor={id}
>
{label}
</label>
<input
id={id}
type={type}
className={className}
onChange={onChange ? (e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value) : undefined}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
value={value}
required={required}
disabled={disabled}
readOnly={readonly}
placeholder={placeholder}
style={{
width: "100%",
height: "43px",
borderBottom: `1px solid ${getBorderColor()}`,
display: "flex",
flexDirection: "row",
alignItems: "center",
color: "#111",
fontSize: "14px",
fontStyle: "normal",
fontWeight: "500",
lineHeight: "140%",
letterSpacing: "-0.28px",
outline: "none",
transition: "border-color 0.2s ease-in-out",
...style,
}}
Comment thread
seungdeok marked this conversation as resolved.
/>
<span
style={{
color: "#E52929",
position: "absolute",
bottom: "-24px",
left: "0",
fontSize: "12px",
fontWeight: "500",
lineHeight: "140%",
letterSpacing: "-0.24px",
}}
>
{displayError}
</span>
Comment on lines +91 to +104

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

에러 메시지 위치가 다른 요소와 겹칠 수 있습니다.

position: "absolute"bottom: "-24px"를 사용하여 에러 메시지를 배치하면, TextField 하단에 다른 요소가 있을 경우 겹쳐 보일 수 있습니다. 부모 컴포넌트에서 적절한 간격을 확보해야 합니다.

다음 중 하나를 고려하세요:

  1. 부모 div에 margin-bottom 또는 padding-bottom을 추가하여 공간 확보
  2. 에러 메시지를 문서 흐름 내에 배치(position: static)하여 자동으로 공간 확보
🤖 Prompt for AI Agents
In apps/web/src/views/login/components/TextField.tsx around lines 91 to 104, the
error message is absolutely positioned (position: "absolute", bottom: "-24px")
which can overlap adjacent elements; change the layout so the error does not
overlap by either (a) moving the error span into the document flow (remove
position/bottom and use default/static positioning) so it pushes content and
reserves space, or (b) keep absolute positioning but add appropriate bottom
spacing on the parent container (increase parent padding-bottom or margin-bottom
by at least 24px) and ensure the parent is positioned (relative) so the absolute
positioning is scoped; pick one approach and update styles accordingly.

</div>
);
}
19 changes: 1 addition & 18 deletions apps/web/src/views/login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
"use client";

import GoogleButton from "@/widgets/login/GoogleButton";
import { useSession } from "next-auth/react";

export function LoginPage() {
const { data: session } = useSession();

return (
<div
style={{
Expand All @@ -16,19 +11,7 @@ export function LoginPage() {
height: "100vh",
}}
>
<GoogleButton />
<div style={{ width: 300, marginTop: 20 }}>
<pre
style={{
whiteSpace: "pre-wrap",
wordWrap: "break-word",
overflowWrap: "break-word",
maxWidth: "100%",
}}
>
{JSON.stringify(session, null, 2)}
</pre>
</div>
<div>Login</div>
</div>
);
}
103 changes: 103 additions & 0 deletions apps/web/src/views/login/login.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import React from "react";
import { LoginPage } from ".";
import { Button } from "./components/Button";
import { SNSLoginButton } from "./components/SNSLoginButton";
import { TextField } from "./components/TextField";

const meta = {
title: "v2/Views/Login",
component: LoginPage,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {},
} satisfies Meta<typeof LoginPage>;

export default meta;
type Story = StoryObj<typeof meta>;

// 구글 로그인
export const 구글_로그인: Story = {
args: {},
render: () => (
<div style={{ width: "360px" }}>
<SNSLoginButton provider="google" />
</div>
),
};

// 카카오 로그인
export const 카카오_로그인: Story = {
args: {},
render: () => (
<div style={{ width: "360px" }}>
<SNSLoginButton provider="kakao" />
</div>
),
};

// 이메일 주소 입력
export const 이메일_주소_입력: Story = {
args: {},
render: () => {
const [email, setEmail] = React.useState("");

return (
<div style={{ width: "360px" }}>
<TextField
type="text"
label="이메일 주소"
placeholder="abcdef@naver.com"
value={email}
onChange={setEmail}
error="잘못된 이메일 주소입니다."
validate={value => {
// 빈 값이거나 유효한 이메일 형식인지 확인
if (value.length === 0) return true;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value);
}}
/>
</div>
);
},
};

// 비밀번호 입력
export const 비밀번호_입력: Story = {
args: {},
render: () => {
const [password, setPassword] = React.useState("");

return (
<div style={{ width: "360px" }}>
<TextField
type="password"
label="비밀번호"
placeholder="8~16자리 영대•소문자, 숫자, 특수문자 조합"
value={password}
onChange={setPassword}
error="잘못된 비밀번호입니다."
validate={value => {
// 8~16자리 영대•소문자, 숫자, 특수문자 조합
if (value.length === 0) return true;
return value.length >= 8 && value.length <= 16;
}}
/>
</div>
);
},
};

// 로그인 버튼
export const 로그인_버튼: Story = {
args: {},
render: () => (
<div style={{ width: "360px", display: "flex", flexDirection: "column", gap: "12px" }}>
<Button label="로그인" />
<Button label="로그인" disabled />
</div>
),
};
17 changes: 17 additions & 0 deletions apps/web/src/views/register/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client";

export function RegisterPage() {
return (
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "100vh",
}}
>
<div>Register</div>
</div>
);
}
Loading