Repository: Fit-Links/FitLink-FE
Pull request #115: [FLIZ-187/ui] feat: 휴대폰 인증 공통 컴포넌트 구현
Author: login: MaSeungHyun
Labels: name: ✨feat
description: 새로운 기능 추가
Created: 2025-04-06T08:51:09Z | Merged: 2025-04-15T07:08:39Z

## 📝 작업 내용

휴대폰 기기 인증을 위한 컴포넌트를 구현하였습니다

* 휴대폰 번호를 저장하기 위한 기능으로 trainer와 user에서 휴대폰 저장을 위한 공통 기능을 수행하는 것으로 파악하여 공통 컴포넌트로 작성하였습니다.

* (개발 중 수정사항) 바닐라JS로 구현하려하던 부분을  리액트 철학에 좀 더 적합하게 개발하고자 선언형으로 구현하여 useRef를 통하여 해당 돔에 접근하여 SMS 이동 이벤트를 발생시키고자 하였습니다.


### 📷 스크린샷 (선택)

<img width="528" alt="스크린샷 2025-04-06 오후 5 47 18" src="https://github.com/user-attachments/assets/9009a836-f135-4857-9b04-6b8ac5db11e2" />

## 💬 리뷰 요구사항(선택)

정보를 검색하던 중 `📄 *.webp`보다 `📄 *.avif`의 압축률이 더 좋다고 정보를 찾아 `avif` 확장자의 파일을 사용하였습니다.

Header에서 `back`이라는 props를 받아 헤더에서 버튼의 유무를 구현할까하다가 그럼 컴포넌트가 아닌 페이지 개발의 개념이 되는 것 같아 Header는 불포함 시켜서 개발하였습니다.



Diff:
diff --git a/packages/ui/src/assets/verify-phone.avif b/packages/ui/src/assets/verify-phone.avif
new file mode 100644
index 00000000..8dfacb25
Binary files /dev/null and b/packages/ui/src/assets/verify-phone.avif differ
diff --git a/packages/ui/src/components/PhoneVerification/PhoneVerificationButton.tsx b/packages/ui/src/components/PhoneVerification/PhoneVerificationButton.tsx
new file mode 100644
index 00000000..7ff2bfff
--- /dev/null
+++ b/packages/ui/src/components/PhoneVerification/PhoneVerificationButton.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import { ComponentProps, forwardRef } from "react";
+
+import { Button } from "../Button";
+
+type PhoneVerificationButtonProps = ComponentProps<"button">;
+
+export const PhoneVerificationButton = forwardRef<HTMLAnchorElement, PhoneVerificationButtonProps>(
+  (props, ref) => {
+    return (
+      <section className="flex flex-1 flex-col justify-end pb-[2.125rem]">
+        <a ref={ref} className="hidden" aria-label="verification-link" />
+        <Button className="text-headline  h-[3.375rem] w-full" onClick={props.onClick}>
+          인증 메시지 보내기
+        </Button>
+      </section>
+    );
+  },
+);
+
+PhoneVerificationButton.displayName = "PhoneVerificationButton";
diff --git a/packages/ui/src/components/PhoneVerification/PhoneVerificationGuide.tsx b/packages/ui/src/components/PhoneVerification/PhoneVerificationGuide.tsx
new file mode 100644
index 00000000..b1f461db
--- /dev/null
+++ b/packages/ui/src/components/PhoneVerification/PhoneVerificationGuide.tsx
@@ -0,0 +1,11 @@
+import React from "react";
+
+export default function PhoneVerificationGuide() {
+  return (
+    <div className="text-body-1 mt-[1.875rem] flex w-full flex-col gap-[1.875rem]  px-[1.875rem]">
+      <p>① 하단 [인증 메시지 보내기] 눌러주세요.</p>
+      <p>② 메시지 작성 창에서 인증 메시지가 자동으로 입력되어 있어요.</p>
+      <p>③ 인증 메시지를 그대로 보내주세요.</p>
+    </div>
+  );
+}
diff --git a/packages/ui/src/components/PhoneVerification/PhoneVerificationImage.tsx b/packages/ui/src/components/PhoneVerification/PhoneVerificationImage.tsx
new file mode 100644
index 00000000..a4e3a0c5
--- /dev/null
+++ b/packages/ui/src/components/PhoneVerification/PhoneVerificationImage.tsx
@@ -0,0 +1,7 @@
+import Image from "next/image";
+
+import verifyPhone from "@ui/assets/verify-phone.avif";
+
+export default function PhoneVerificationImage() {
+  return <Image className="mt-[2.5rem] px-[0.125rem]" src={verifyPhone} alt="verify-phone" />;
+}
diff --git a/packages/ui/src/components/PhoneVerification/PhoneVerificationNotice.tsx b/packages/ui/src/components/PhoneVerification/PhoneVerificationNotice.tsx
new file mode 100644
index 00000000..255f2e29
--- /dev/null
+++ b/packages/ui/src/components/PhoneVerification/PhoneVerificationNotice.tsx
@@ -0,0 +1,8 @@
+export default function PhoneVerificationNotice() {
+  return (
+    <div className="text-body-2 text-text-sub2 mt-[2.5rem] w-full text-center">
+      <p>이용 중인 통신 요금제에 따라 문자 메시지 발송 비용이</p>
+      <p>청구될 수 있어요.</p>
+    </div>
+  );
+}
diff --git a/packages/ui/src/components/PhoneVerification/index.tsx b/packages/ui/src/components/PhoneVerification/index.tsx
new file mode 100644
index 00000000..9565394a
--- /dev/null
+++ b/packages/ui/src/components/PhoneVerification/index.tsx
@@ -0,0 +1,7 @@
+type PhoneVerificationProps = {
+  children: React.ReactNode;
+};
+
+export default function PhoneVerification({ children }: PhoneVerificationProps) {
+  return <section className="flex h-full w-full flex-col">{children}</section>;
+}
diff --git a/packages/ui/src/types/image.d.ts b/packages/ui/src/types/image.d.ts
new file mode 100644
index 00000000..5e5a206d
--- /dev/null
+++ b/packages/ui/src/types/image.d.ts
@@ -0,0 +1,6 @@
+declare module "next/image";
+
+declare module "*.avif" {
+  const value: string;
+  export default value;
+}
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
index 02ade59f..47ff0b1d 100644
--- a/packages/ui/tsconfig.json
+++ b/packages/ui/tsconfig.json
@@ -8,6 +8,6 @@
     },
     "types": ["node", "jest", "@testing-library/jest-dom"]
   },
-  "include": ["."],
+  "include": [".", "./**/*.d.ts"],
   "exclude": ["dist", "build", "node_modules"]
 }


Reviews:
user: login: choi-ik
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-08T16:56:04Z
user: login: choi-ik
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T02:38:46Z
user: login: MaSeungHyun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T09:29:06Z
user: login: MaSeungHyun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T09:29:27Z
user: login: MaSeungHyun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T09:31:25Z
user: login: MaSeungHyun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-12T09:34:06Z
user: login: choi-ik
body: LGTM 🚀
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-14T11:45:10Z

Review comments:
diff_hunk: @@ -0,0 +1,17 @@
+"use client";
path: packages/ui/src/components/PhoneVerification/index.tsx
user: login: choi-ik
body: ui 모듈은 react로 구성되어 있어서 use client 작성 안하셔도 됩니다!
created_at: 2025-04-08T08:31:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,10 @@
+import React from "react";
+
+export default function PhoneVerificationNotice() {
+  return (
+    <main className="text-body-2 text-text-sub2 mt-[2.5rem] w-full text-center">
+      <p>이용 중인 통신 요금제에 따라 문자 메시지 발송 비용이</p>
+      <p>청구될 수 있어요.</p>
+    </main>
path: packages/ui/src/components/PhoneVerification/PhoneVerificationNotice.tsx
user: login: choi-ik
body: 컴포넌트 단위에는 main 태그는 쓰지 않는 것이 좋아보입니다. main 태그는 문서당 하나만 있는 것이 시맨틱하다고 생각돼요!
created_at: 2025-04-08T08:50:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,29 @@
+import { useRef } from "react";
+
+import { Button } from "../Button";
+
+export default function PhoneVerificationButton() {
+  const verificationRef = useRef<HTMLAnchorElement>(null);
+
+  const handleClickMoveToVerification = () => {
+    if (!verificationRef.current) return;
+
+    // TODO
+    // 토큰 받아와서 HyperTextReference 에 적용
+    // verificationRef.current.href = "sms:tpdlqj0514@gmail.com?body=TEST TOKEN";
path: packages/ui/src/components/PhoneVerification/PhoneVerificationButton.tsx
user: login: choi-ik
body: 저는 ui 모듈에 위치한 컴포넌트에서 직접 API를 불러오는 것은 좋지 않다고 생각합니다.

우선 ui 모듈은 ui라는 관심사를 가지고 있는 컴포넌트들이 집합된 공간입니다.

즉, `data fetching`이라는 관심사와 완전히 분리되어 UI 라는 관심사만 가지고 있는 모듈을 모아놓은 공간이라고 생각이 되는데, 현재 승현님께서 설계하신 부분은 data fetching과 이를 통한 비즈니스 로직을 ui 모듈내에 위치한 컴포넌트에서 구현하시려는 것으로 예상이돼요.

우선 지금까지는 트레이너와 유저 도메인에서 기기 인증 화면은 동일한 UI를 가지고 있어 큰 문제는 없습니다.

다만, 우리가 UI라는 모듈을 만든 목적을 생각해본다면 정확히 UI와 관련된 관심사를 가지고 있는 컴포넌트들을 여기저기서 자유롭게 사용할 수 있는, 즉 확장성이 높은 컴포넌트들을 모아두는 것이 가장 큰 목적입니다. 조금 먼 얘기지만 우리가 만약 새로운 도메인이 생겨 새로 생긴 도메인에서 기기 인증을 진행하는데 조금 다른 data fethching을 요구한다면 현재 이 컴포넌트는 확장하기 어려울 것이라고 예상이됩니다.

따라서 위와 같은 경우는 너무 먼 미래를 예상한 것 이기는 하지만, ui라는 모듈을 만든 목적에 맞게 설계하는 것이 중요하다고 생각이돼요.

created_at: 2025-04-08T16:52:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+"use client";
+
+import PhoneVerificationButton from "./PhoneVerificationButton";
+import PhoneVerificationGuide from "./PhoneVerificationGuide";
+import PhoneVerificationImage from "./PhoneVerificationImage";
+import PhoneVerificationNotice from "./PhoneVerificationNotice";
+
+export default function PhoneVerification() {
+  return (
+    <main className="flex h-full w-full flex-col">
+      <PhoneVerificationGuide />
+      <PhoneVerificationImage />
+      <PhoneVerificationNotice />
+      <PhoneVerificationButton />
+    </main>
path: packages/ui/src/components/PhoneVerification/index.tsx
user: login: choi-ik
body: 해당 컴포넌트는 페이지가 아닌 컴포넌트이기 떄문에 main은 안쓰는 것이 좋아보입니다.
created_at: 2025-04-08T16:55:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,29 @@
+import { useRef } from "react";
+
+import { Button } from "../Button";
+
+export default function PhoneVerificationButton() {
+  const verificationRef = useRef<HTMLAnchorElement>(null);
+
+  const handleClickMoveToVerification = () => {
+    if (!verificationRef.current) return;
+
+    // TODO
+    // 토큰 받아와서 HyperTextReference 에 적용
+    // verificationRef.current.href = "sms:tpdlqj0514@gmail.com?body=TEST TOKEN";
path: packages/ui/src/components/PhoneVerification/PhoneVerificationButton.tsx
user: login: choi-ik
body: 위 내용 읽으시기 전에, Todo 주석이 API Call을 통해 토큰을 받아오는 로직을 작성 하겠다는 내용이 맞을까요??
created_at: 2025-04-09T02:38:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,17 @@
+"use client";
path: packages/ui/src/components/PhoneVerification/index.tsx
user: login: MaSeungHyun
body: useRef를 사용하는 Button에만 선언하겠습니다.
created_at: 2025-04-12T09:29:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,10 @@
+import React from "react";
+
+export default function PhoneVerificationNotice() {
+  return (
+    <main className="text-body-2 text-text-sub2 mt-[2.5rem] w-full text-center">
+      <p>이용 중인 통신 요금제에 따라 문자 메시지 발송 비용이</p>
+      <p>청구될 수 있어요.</p>
+    </main>
path: packages/ui/src/components/PhoneVerification/PhoneVerificationNotice.tsx
user: login: MaSeungHyun
body: 레이아웃을 위해 div 태그로 대체하였습니다
created_at: 2025-04-12T09:29:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,17 @@
+"use client";
+
+import PhoneVerificationButton from "./PhoneVerificationButton";
+import PhoneVerificationGuide from "./PhoneVerificationGuide";
+import PhoneVerificationImage from "./PhoneVerificationImage";
+import PhoneVerificationNotice from "./PhoneVerificationNotice";
+
+export default function PhoneVerification() {
+  return (
+    <main className="flex h-full w-full flex-col">
+      <PhoneVerificationGuide />
+      <PhoneVerificationImage />
+      <PhoneVerificationNotice />
+      <PhoneVerificationButton />
+    </main>
path: packages/ui/src/components/PhoneVerification/index.tsx
user: login: MaSeungHyun
body: 기능을 전체를 뜻하는 의미로 `section` 태그로 대체하였습니다.
created_at: 2025-04-12T09:31:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,29 @@
+import { useRef } from "react";
+
+import { Button } from "../Button";
+
+export default function PhoneVerificationButton() {
+  const verificationRef = useRef<HTMLAnchorElement>(null);
+
+  const handleClickMoveToVerification = () => {
+    if (!verificationRef.current) return;
+
+    // TODO
+    // 토큰 받아와서 HyperTextReference 에 적용
+    // verificationRef.current.href = "sms:tpdlqj0514@gmail.com?body=TEST TOKEN";
path: packages/ui/src/components/PhoneVerification/PhoneVerificationButton.tsx
user: login: MaSeungHyun
body: 네 말씀하신 내용 맞습니다. 제가 개념이 부족하여 찾아본 후 수정하겠습니다.
created_at: 2025-04-12T09:34:06Z
author_association: CONTRIBUTOR

PR comments:
user: login: choi-ik
created_at: 2025-04-08T08:15:59Z
body: PR 올리실때 Reviewer에 저랑 용재님 추가해주셔야 저희한테 PR 알림이 옵니다!
author_association: COLLABORATOR
user: login: MaSeungHyun
created_at: 2025-04-12T10:05:31Z
body: ✅ 컴포넌트 역할의 주요 기능을 표시하기 위한 index에는 section태그로 변경하였습니다. 그 외, 컴포넌트는 section에서 div태그를 통하여 레이아웃 태그로 변경하였습니다.

✅ 상위로 이벤트 주입을 위하여 props를 통한 이벤트를 적용하였으며, a 태그에 접근하기 위하여 forwardRef를 통하여 상위에서 a태그에 접근할 수 있도록 하였습니다.

✅ 비즈니스 로직을 상위에서의  주입을 위하여 `props`를 통해 또 그 안에서 `onClick` `props`를 넘겨주는 것은 ⚠️ `드릴링`이 발생하는 것으로 생각되어 합성 컴포넌트로 변경하여, 상위 컴포넌트에서 하위 컴포넌트를 선언하여 해당 컴포넌트에 바로 주입할 수 있도록 변경하였습니다.

author_association: CONTRIBUTOR