ewhk9887/korean_code_reviews_from_github huggingface dataset:
@@ -3,16 +3,12 @@
     <section
       class="mx-auto flex h-full flex-col items-center justify-between bg-gray_00-light text-gray_05-light dark:bg-gray_00-dark dark:text-gray_05-dark sm:w-screen md:w-96"
     >
-      <ProgressNavBar prevPage="/size" pageName="ingredient" />
-      <div v-if="isLoading">로딩중입니다.</div>
+      <ProgressNavBar prevPage="size" pageName="ingredient" />
+      <Loading v-if="isLoading" />
       <IngredientBoard :ingredients="data?.ingredients ?? []" v-else />
       <div v-if="!isAbleToRecommend">재료를 조금 더 골라볼까요?</div>
       <NextButton
-        :onClick="
-          () => {
-            mutation.mutate(userItem);
-          }
-        "
+        :onClick="postUserItem"
         isPrimary
         :disabled="!isAbleToRecommend || !userSelect.sizeValue"
       >
@@ -29,6 +25,7 @@ import { storeToRefs } from 'pinia';
 import ProgressNavBar from '@containers/ProgressNavBar.vue';
 import IngredientBoard from '@containers/IngredientBoard.vue';
 import NextButton from '@components/NextButton.vue';
+import Loading from '@src/components/Loading.vue';
 import { useUserSelectStore } from '@store/storeUserSelect';
 import { useGetIngredients } from '@apis/ingredients';
 import { usePostRecipe } from '@apis/recipes';
@@ -38,6 +35,9 @@ const { userSelect, userItem } = storeToRefs(store);
 
 const { data, isLoading } = useGetIngredients();
 const mutation = usePostRecipe();
+const postUserItem = () => {
+  mutation.mutate(userItem.value);
+};
 
 const allFlavorIdList = computed(
   () =>

리뷰: 서버 상태관리와 클라이언트 상태관리를 분리하여 관리하셨군요 👍
@@ -1,23 +1,20 @@
 <template>
-  <button
-    @click="
-      () => {
-        if (!props.disabled) props.onClick();
-      }
-    "
-    :class="getNextButtonStyle()"
-  >
-    <slot></slot>
+  <button @click="handleNextButtonClick" :class="getNextButtonStyle()">
+    <slot />
   </button>
 </template>
 
 <script setup lang="ts">
 const props = defineProps<{
-  isPrimary?: boolean;
+  isPrimary?: boolean | undefined;
   onClick: () => void;
-  disabled?: boolean;
+  disabled?: boolean | undefined;
 }>();
 
+const handleNextButtonClick = () => {
+  if (!props.disabled) props.onClick();
+};
+
 const getNextButtonStyle = () => ({
   'h-fit w-[340px] rounded-2xl border-0 p-6 text-large font-bold mb-6': true,
   'bg-primary-light dark:bg-primary-dark': props.isPrimary,

리뷰: 사실 저도 추가하지 않았다가, 좀더 가시적으로 표현하려고 작성을 했어요! 음, 메이브님 말씀듣고 다시 생각해보니 가시성만으로 추가하는 건 좀 의미가 없는 것 같긴하네요!!
@@ -3,20 +3,18 @@
     class="text-gray_05-light dark:text-gray_05-dark w-6 h-6"
     @click="goBack"
   >
-    <slot></slot>
+    <slot />
   </button>
 </template>
 
 <script setup lang="ts">
-import { useRouter } from 'vue-router';
+import { pushPage } from '@router/route.helper';
 
 const props = defineProps<{
   prevPage: string;
 }>();
 
-const router = useRouter();
-
 function goBack() {
-  router.push(props.prevPage);
+  pushPage(props.prevPage);
 }
 </script>

리뷰: 화살표 함수로 해주세요~
@@ -0,0 +1,30 @@
+package christmas.Util;
+
+public class CommaFormatter {
+    public static String formatWithComma(int number) {
+        String numberStr = Integer.toString(number);
+        int length = numberStr.length();
+
+        if (length <= 3) {
+            return numberStr;
+        }
+        String result = insertComma(numberStr, length);
+
+        return result;
+    }
+
+    private static String insertComma(String number, int length) {
+        int count = 0;
+        StringBuilder result = new StringBuilder();
+        for (int i = length - 1; i >= 0; i--) {
+            result.insert(0, number.charAt(i));
+            count++;
+
+            if (count == 3 && i != 0) {
+                result.insert(0, ',');
+                count = 0;
+            }
+        }
+        return result.toString();
+    }
+}

리뷰: 콤마를 위해 전용 객체를 만들어주셨군요! 직접 만들어주는 것도 좋네요!
그런데 돈 단위를 위한 콤마를 넣어주는 기능을 자바에서 제공하고 있답니다! ㅎㅎ
DecimalFormat 에 대해서 알아보시면 좋을 것 같아요! 제가 [참고했던 글](https://jamesdreaming.tistory.com/203)을 공유합니다!
@@ -0,0 +1,67 @@
+package christmas.Domain;
+
+import christmas.Util.OrderMenuValidator;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class EventPlanner {
+    private final List<Menu> orderMenu;
+
+    public EventPlanner(String menu) {
+        validateOrderMenuForm(menu);
+        List<Menu> orderMenu = createOrderMenuRepository(menu);
+        this.orderMenu = orderMenu;
+    }
+
+    public int calculatePreDiscountTotalOrderPrice() {
+        int totalOrderPrice = 0;
+        for (Menu menu : orderMenu) {
+            totalOrderPrice += menu.getMenuTotalPrice();
+        }
+        return totalOrderPrice;
+    }
+
+    public Map<String, Integer> calculateCategoryCount() {
+        Map<String, Integer> categoryCount = new HashMap<>();
+        int count = 0;
+        for (Menu menu : orderMenu) {
+            String menuCategory = menu.getMenuCategory();
+            if (categoryCount.containsKey(menuCategory)) {
+                count = categoryCount.get(menuCategory) + menu.getMenuQuantity();
+                categoryCount.put(menuCategory, count);
+            }
+            if (!categoryCount.containsKey(menuCategory)) {
+                categoryCount.put(menuCategory, menu.getMenuQuantity());
+            }
+        }
+        return categoryCount;
+    }
+
+    private List<Menu> createOrderMenuRepository(String orderMenu) {
+        List<Menu> menuList = new ArrayList<>();
+        String pattern = "([가-힣]+)-([1-9]\\d*|0*[1-9]\\d+)(?:,|$)";
+
+        // 정규표현식에 맞는 패턴을 생성
+        Pattern regexPattern = Pattern.compile(pattern);
+        Matcher matcher = regexPattern.matcher(orderMenu);
+
+        // 매칭된 결과를 출력
+        while (matcher.find()) {
+            String menuName = matcher.group(1);
+            String menuQuantity = matcher.group(2);
+            menuList.add(new Menu(menuName, menuQuantity));
+        }
+        return menuList;
+    }
+
+    private void validateOrderMenuForm(String orderedMenu) {
+        OrderMenuValidator.checkValidOrderForm(orderedMenu);
+        OrderMenuValidator.checkDuplicateMenu(orderedMenu);
+        OrderMenuValidator.checkMaxOrderQuantity(orderedMenu);
+        OrderMenuValidator.checkMenuContainsOnlyDrink(orderedMenu);
+    }
+}

리뷰: ```suggestion
    public Map<String, Integer> calculateCategoryCount() {
        Map<String, Integer> categoryCount = new HashMap<>();
        int count = 0;
        for (Menu menu : orderMenu) {
            String menuCategory = menu.getMenuCategory();
            categoryCount.put(menuCategory, categoryCount.getOrDefault(menuCategory, 0) + 1)
        }
        return categoryCount;
    }
```
`getOrDefault` 를 사용한다면 간단하게 바꿔줄 수 있답니다! ㅎㅎ
값이 있다면 해당 값을 가져오고 없으면 default 로 지정해준 값(여기선 0)을 가져오는 기능입니다!
@@ -47,4 +47,13 @@ final class SettingViewModel {
             seconds: 0,
             identifier: "SelectedTimeNotification")
     }
+    
+    // URL 열기 메서드
+    func openURL(_ urlString: String) {
+        guard let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) else {
+            print("유효하지 않은 URL입니다: \(urlString)")
+            return
+        }
+        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+    }
 }

리뷰: url 이동이 이렇게 구현하는 거군요
@@ -0,0 +1,31 @@
+package com.spoteditor.backend.domain.user.controller;
+
+import com.spoteditor.backend.common.exceptions.user.UserException;
+import com.spoteditor.backend.common.exceptions.user.UserErrorCode;
+import com.spoteditor.backend.domain.user.service.UserTokenService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final UserTokenService userTokenService;
+
+    @PostMapping("/auth/refresh")
+    public ResponseEntity<Void> refreshAccessToken (HttpServletRequest request, HttpServletResponse response) throws Exception {
+        userTokenService.refreshAccessToken(request, response);
+
+        return new ResponseEntity<>(HttpStatus.OK);
+    }
+}

리뷰: BaseException이 RuntimeException을 상속받는 저희 프로젝트의 최상위 예외가 맞다면 직접 노출을 할 필요가 없어보입니다.
최상위 언체크 예외를 개발 후 그 언체크 예외를 상속받고 각각의 커스텀 오류 코드를 등록한다면 될 것 같습니다.
예외 처리는 세밀하게 하는 것이 좋습니다.
@@ -0,0 +1,41 @@
+package com.spoteditor.backend.domain.user.service;
+
+import com.spoteditor.backend.common.exceptions.user.UserErrorCode;
+import com.spoteditor.backend.common.exceptions.user.UserException;
+import com.spoteditor.backend.common.util.CookieUtils;
+import com.spoteditor.backend.domain.user.common.dto.UserIdDto;
+import com.spoteditor.backend.security.jwt.JwtConstants;
+import com.spoteditor.backend.security.jwt.JwtUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class UserTokenService {
+
+    private final JwtUtils jwtUtils;
+    private final CookieUtils cookieUtils;
+
+    public void refreshAccessToken(HttpServletRequest request, HttpServletResponse response) throws Exception {
+        String refreshToken = cookieUtils.getRefreshToken(request);
+
+        if(refreshToken == null) {
+            throw new UserException(UserErrorCode.REFRESH_TOKEN_NOT_IN_COOKIE);
+        }
+
+        try {
+            // RefreshToken 검증
+            UsernamePasswordAuthenticationToken authentication = jwtUtils.setAuthentication(refreshToken);
+            UserIdDto userIdDto = (UserIdDto) authentication.getPrincipal();
+
+            // 검증 성공 -> accessToken 발급
+            String accessToken = jwtUtils.createAccessToken(userIdDto.getId());
+            cookieUtils.setAccessTokenCookie(response, JwtConstants.ACCESS_TOKEN, accessToken);
+        } catch (UserException e) {
+            throw new UserException(UserErrorCode.REFRESH_TOKEN_INVALID);
+        }
+    }
+}

리뷰: 이 부분도 동일한 맥락입니다.
@@ -0,0 +1,43 @@
+package com.spoteditor.backend.security.oauth.handler;
+
+import com.spoteditor.backend.common.util.CookieUtils;
+import com.spoteditor.backend.security.jwt.JwtConstants;
+import com.spoteditor.backend.security.jwt.JwtUtils;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.util.Map;
+
+@Component
+@RequiredArgsConstructor
+public class OauthSuccessHandler implements AuthenticationSuccessHandler {
+
+    private final JwtUtils jwtUtils;
+    private final CookieUtils cookieUtils;
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+        OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
+
+        Map<String, Object> attributesMap = oauthUser.getAttributes();
+
+        Long id = (Long) attributesMap.get("id");
+        String name = (String) attributesMap.get("name");
+
+        String accessToken = jwtUtils.createAccessToken(id);
+        String refreshToken = jwtUtils.createRefreshToken(id);
+
+        cookieUtils.setAccessTokenCookie(response, JwtConstants.ACCESS_TOKEN, accessToken);
+        cookieUtils.setRefreshTokenCookie(response, JwtConstants.REFRESH_TOKEN, refreshToken);
+
+//        redirect
+        response.sendRedirect("/success");
+    }
+}

리뷰: `/success` 경로가 콜백 처리인가요?
@@ -0,0 +1,43 @@
+package com.spoteditor.backend.security.oauth.handler;
+
+import com.spoteditor.backend.common.util.CookieUtils;
+import com.spoteditor.backend.security.jwt.JwtConstants;
+import com.spoteditor.backend.security.jwt.JwtUtils;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.util.Map;
+
+@Component
+@RequiredArgsConstructor
+public class OauthSuccessHandler implements AuthenticationSuccessHandler {
+
+    private final JwtUtils jwtUtils;
+    private final CookieUtils cookieUtils;
+
+    @Override
+    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+        OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
+
+        Map<String, Object> attributesMap = oauthUser.getAttributes();
+
+        Long id = (Long) attributesMap.get("id");
+        String name = (String) attributesMap.get("name");
+
+        String accessToken = jwtUtils.createAccessToken(id);
+        String refreshToken = jwtUtils.createRefreshToken(id);
+
+        cookieUtils.setAccessTokenCookie(response, JwtConstants.ACCESS_TOKEN, accessToken);
+        cookieUtils.setRefreshTokenCookie(response, JwtConstants.REFRESH_TOKEN, refreshToken);
+
+//        redirect
+        response.sendRedirect("/success");
+    }
+}

리뷰: 네 맞습니다
@@ -0,0 +1,31 @@
+package com.spoteditor.backend.domain.user.controller;
+
+import com.spoteditor.backend.common.exceptions.user.UserException;
+import com.spoteditor.backend.common.exceptions.user.UserErrorCode;
+import com.spoteditor.backend.domain.user.service.UserTokenService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final UserTokenService userTokenService;
+
+    @PostMapping("/auth/refresh")
+    public ResponseEntity<Void> refreshAccessToken (HttpServletRequest request, HttpServletResponse response) throws Exception {
+        userTokenService.refreshAccessToken(request, response);
+
+        return new ResponseEntity<>(HttpStatus.OK);
+    }
+}

리뷰: 답변주신 내용이 BaseException을 상속받는 각각의 커스텀 오류 클래스를 생성해서 더 의미를 명확하게 하라는 말씀이신가요?
@@ -0,0 +1,31 @@
+package com.spoteditor.backend.domain.user.controller;
+
+import com.spoteditor.backend.common.exceptions.user.UserException;
+import com.spoteditor.backend.common.exceptions.user.UserErrorCode;
+import com.spoteditor.backend.domain.user.service.UserTokenService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final UserTokenService userTokenService;
+
+    @PostMapping("/auth/refresh")
+    public ResponseEntity<Void> refreshAccessToken (HttpServletRequest request, HttpServletResponse response) throws Exception {
+        userTokenService.refreshAccessToken(request, response);
+
+        return new ResponseEntity<>(HttpStatus.OK);
+    }
+}

리뷰: 네, 지금 코드를 보시면 BaseException을 그대로 반환하도록 되어 있는데 추가 요구사항 접수시 유지보수가 어려울 수 있습니다.
@@ -0,0 +1,31 @@
+package com.spoteditor.backend.domain.user.controller;
+
+import com.spoteditor.backend.common.exceptions.user.UserException;
+import com.spoteditor.backend.common.exceptions.user.UserErrorCode;
+import com.spoteditor.backend.domain.user.service.UserTokenService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api")
+@RequiredArgsConstructor
+public class AuthController {
+
+    private final UserTokenService userTokenService;
+
+    @PostMapping("/auth/refresh")
+    public ResponseEntity<Void> refreshAccessToken (HttpServletRequest request, HttpServletResponse response) throws Exception {
+        userTokenService.refreshAccessToken(request, response);
+
+        return new ResponseEntity<>(HttpStatus.OK);
+    }
+}

리뷰: 그리고 추가적으로 말씀을 드리는건 저희 간트 차트에 있는 규약을 다시 한 번 이해를 하셔야 할 것 같습니다
POST 요청인 경우에는 분명 바디가 없이 HTTP Status Code를 명확하게 보내줘야하는데 오히려 List 컬렉션을 반환하고 있습니다
@@ -0,0 +1,41 @@
+package com.spoteditor.backend.domain.user.service;
+
+import com.spoteditor.backend.common.exceptions.user.UserErrorCode;
+import com.spoteditor.backend.common.exceptions.user.UserException;
+import com.spoteditor.backend.common.util.CookieUtils;
+import com.spoteditor.backend.domain.user.common.dto.UserIdDto;
+import com.spoteditor.backend.security.jwt.JwtConstants;
+import com.spoteditor.backend.security.jwt.JwtUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class UserTokenService {
+
+    private final JwtUtils jwtUtils;
+    private final CookieUtils cookieUtils;
+
+    public void refreshAccessToken(HttpServletRequest request, HttpServletResponse response) throws Exception {
+        String refreshToken = cookieUtils.getRefreshToken(request);
+
+        if(refreshToken == null) {
+            throw new UserException(UserErrorCode.REFRESH_TOKEN_NOT_IN_COOKIE);
+        }
+
+        try {
+            // RefreshToken 검증
+            UsernamePasswordAuthenticationToken authentication = jwtUtils.setAuthentication(refreshToken);
+            UserIdDto userIdDto = (UserIdDto) authentication.getPrincipal();
+
+            // 검증 성공 -> accessToken 발급
+            String accessToken = jwtUtils.createAccessToken(userIdDto.getId());
+            cookieUtils.setAccessTokenCookie(response, JwtConstants.ACCESS_TOKEN, accessToken);
+        } catch (UserException e) {
+            throw new UserException(UserErrorCode.REFRESH_TOKEN_INVALID);
+        }
+    }
+}

리뷰: 이 UserIdDto를 컨트롤러에서 어떻게 활용할 수 있는지를 문서화해주시면 읽어보고 공간 측에 적용하겠습니다.
@@ -1,8 +1,79 @@
 import React, { Component } from 'react';
+import { Link } from 'react-router-dom';
+
+import './Login.scss';
 
 class Login extends Component {
+  constructor() {
+    super();
+    this.state = {
+      id: '',
+      pw: '',
+    };
+  }
+
+  handleInput = event => {
+    const { name, value } = event.target;
+    this.setState({
+      [name]: value,
+    });
+  };
+
+  goToMain = () => {
+    this.props.history.push('/main-Soojeong#');
+  };
+
   render() {
-    return <div>로그인</div>;
+    const isValid =
+      this.state.id.includes('@') &&
+      this.state.id.length >= 5 &&
+      this.state.pw.length >= 8;
+
+    return (
+      <div className="Login">
+        <main>
+          <header>
+            <h1>Westagram</h1>
+          </header>
+
+          <section className="loginInputBox">
+            <h2 className="sr-only">login page</h2>
+            <form>
+              <input
+                name="id"
+                autoComplete="off"
+                onChange={this.handleInput}
+                type="text"
+                id="loginId"
+                value={this.state.id}
+                placeholder="전화번호, 사용자 이름 또는 이메일"
+              />
+              <input
+                name="pw"
+                onChange={this.handleInput}
+                type="password"
+                value={this.state.pw}
+                placeholder="비밀번호"
+              />
+              <button
+                onClick={this.goToMain}
+                type="button"
+                className="loginBtn"
+                disabled={!isValid}
+              >
+                로그인
+              </button>
+            </form>
+          </section>
+
+          <footer>
+            <Link to="" className="findPassword">
+              비밀번호를 잊으셨나요?
+            </Link>
+          </footer>
+        </main>
+      </div>
+    );
   }
 }
 

리뷰: 오 구조분해 할당~
@@ -1,8 +1,79 @@
 import React, { Component } from 'react';
+import { Link } from 'react-router-dom';
+
+import './Login.scss';
 
 class Login extends Component {
+  constructor() {
+    super();
+    this.state = {
+      id: '',
+      pw: '',
+    };
+  }
+
+  handleInput = event => {
+    const { name, value } = event.target;
+    this.setState({
+      [name]: value,
+    });
+  };
+
+  goToMain = () => {
+    this.props.history.push('/main-Soojeong#');
+  };
+
   render() {
-    return <div>로그인</div>;
+    const isValid =
+      this.state.id.includes('@') &&
+      this.state.id.length >= 5 &&
+      this.state.pw.length >= 8;
+
+    return (
+      <div className="Login">
+        <main>
+          <header>
+            <h1>Westagram</h1>
+          </header>
+
+          <section className="loginInputBox">
+            <h2 className="sr-only">login page</h2>
+            <form>
+              <input
+                name="id"
+                autoComplete="off"
+                onChange={this.handleInput}
+                type="text"
+                id="loginId"
+                value={this.state.id}
+                placeholder="전화번호, 사용자 이름 또는 이메일"
+              />
+              <input
+                name="pw"
+                onChange={this.handleInput}
+                type="password"
+                value={this.state.pw}
+                placeholder="비밀번호"
+              />
+              <button
+                onClick={this.goToMain}
+                type="button"
+                className="loginBtn"
+                disabled={!isValid}
+              >
+                로그인
+              </button>
+            </form>
+          </section>
+
+          <footer>
+            <Link to="" className="findPassword">
+              비밀번호를 잊으셨나요?
+            </Link>
+          </footer>
+        </main>
+      </div>
+    );
   }
 }
 

리뷰: this.state.id.includes('@') &&
      this.state.id.length >= 5 &&
      this.state.pw.length >= 8   이걸 true/false Boolean?? 이값을 사용해서 삼항연산자로 해도 되지 않을까요 수정님은 잘하시니 아마가능하실듯!