ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 미신청자/신청자/참가자/작성자 별 컴포넌트 구분하기
    Today I Learned 2022. 11. 20. 23:59

     

    이번 주 Task 목록에서 남은 작업들 중 오늘 가장 먼저 진행한 작업은 사용자는 각 게시글의 운동에 참가신청/신청취소/참가취소를 할 수 있고, 게시글 작성자는 신청자의 참가를 수락하거나 거절할 수 있는 기능을 구현하는 것이었다.

     

    모델 'Register'의 상태 변화를 촉발시키기 위해서는 먼저 사용자마다 자신의 상태에 맞는 화면과 버튼이 주어져서 해당 동작으로 요청을 진행할 수 있어야 했다. 먼저 사용자별로 각기 다른 요구사항을 세웠다.

     

     

    특정 게시글에 신청하지 않은 사용자

    • 게시글 리스트 조회 기능에서 해당 게시글에 대해, 혹은 해당 게시글 상세 내용 조회 시 신청 버튼을 확인할 수 있다.

    특정 게시글에 신청한 사용자

    • 게시글 리스트 조회, 게시글 상세 내용 조회 시 신청취소 버튼을 확인할 수 있다.

    특정 게시글에 참가가 확정된 사용자 

    • 게시글 리스트 조회, 게시글 상세 내용 조회 시 참가취소 버튼을 확인할 수 있다.
    • 게시글 상세 내용 조회 시 참가자 목록에서 사용자 자신의 정보를 확인할 수 있다.

    게시글 작성자

    • 게시글 리스트 조회 시에는 신청, 신청취소, 참가취소 버튼을 확인할 수 없다.
    • 게시글 상세 내용 조회 시에는 참가자 목록과 함께 신청자 목록을 확인할 수 있고, 각 참가자별로 수락, 거절 버튼을 확인할 수 있다.

     

     

    - 프론트엔드 / 백엔드 작업 내역

     

     

    오늘 했던 작업을 순서대로 추적해보았다.

     

    UI

    UI에서 자신이 어떤 사용자인지 판별하기 위해 백엔드에 전달하는 사용자 Access Token에 따라 사용자가 작성자인지 아닌지, 게시글에 신청했는지 혹은 참가하는지를 구분할 수 있는 식별 데이터(isAuthor, registerStatus)를 정의해 판별하도록 UI 구조를 작성했다. 게시글 상세 내역 조회 UI의 경우 다음과 같이 UI 구조가 나뉘도록 했다. 소스코드의 일부분을 첨부했다.

     

    // Post.jsx
    
    export default function Post({
      // data props
      // handler props
    }) {
      const onClickBackward = () => {
        navigateToBackward();
      };
    
      const onClickRegister = () => {
        handleClickRegister(game.id);
      };
    
      const onClickRegisterCancel = () => {
        handleClickRegisterCancel(game.registerId);
      };
    
      const onClickParticipateCancel = () => {
        handleClickParticipateCancel(game.registerId);
      };
    
      // guard clause
    
      return (
        <Container>
          <Backward
            type="button"
            onClick={onClickBackward}
          >
            ⬅️
          </Backward>
          <PostInformation
            // props
          />
          <PostGameInformation
            // props
          />
          <PostMemberInformation
            members={members}
          />
          {post.isAuthor ? (
            <PostGameApplicantsInformation
              applicants={applicants}
              acceptRegister={acceptRegister}
              rejectRegister={rejectRegister}
            />
          ) : (
            <PostRegisterButton
              registerStatus={game.registerStatus}
              onClickRegister={onClickRegister}
              onClickRegisterCancel={onClickRegisterCancel}
              onClickParticipateCancel={onClickParticipateCancel}
            />
          )}
        </Container>
      );
    }
    // PostRegisterButton.jsx
    
    export default function PostRegisterButton({
      registerStatus,
      onClickRegister,
      onClickRegisterCancel,
      onClickParticipateCancel,
    }) {
      if (registerStatus === 'none') {
        return (
          <button
            type="button"
            onClick={onClickRegister}
          >
            신청
          </button>
        );
      }
    
      if (registerStatus === 'processing') {
        return (
          <button
            type="button"
            onClick={onClickRegisterCancel}
          >
            신청취소
          </button>
        );
      }
    
      // if (registerStatus === 'accepted')
      return (
        <button
          type="button"
          onClick={onClickParticipateCancel}
        >
          참가취소
        </button>
      );
    }

     

    Page

    페이지에서는 각 버튼을 누르면 Store에서 API 요청을 호출하는 메서드를 호출한 뒤, 요청 처리에 따라 변화된 데이터를 서버에서 가져와 상태를 갱신하는 메서드를 다시 호출하도록 했다. Store와 ApiService에서는 GET 요청으로 서버에 저장된 데이터를 가져오거나, PATCH 요청으로 Register 인스턴스의 상태 변화를 유도한다.

     

    // PostPage.jsx
    
    export default function PostPage() {
      // useLocalStorage
      // get postId from location state or pathname
    
      const postStore = usePostStore();
      const gameStore = useGameStore();
      const registerStore = useRegisterStore();
    
      const fetchData = async () => {
        await postStore.fetchPost(postId);
        const gameId = await gameStore.fetchGame(postId);
        const { isAuthor } = postStore.post;
        if (gameId && !isAuthor) {
          await registerStore.fetchMembers(gameId);
        }
        if (gameId && isAuthor) {
          await registerStore.fetchMembers(gameId);
          await registerStore.fetchApplicants(gameId);
        }
      };
    
      useEffect(() => {
        fetchData(postId);
      }, [accessToken]);
    
      const { post } = postStore;
      const { game } = gameStore;
      const { members, applicants } = registerStore;
    
      // navigate to backward
    
      const handleClickRegister = async (gameId) => {
        const applicationId = await registerStore.registerToGame(gameId);
        if (applicationId) {
          await fetchData(postId);
        }
      };
    
      const handleClickRegisterCancel = async (registerId) => {
        await registerStore.cancelRegisterToGame(registerId);
        await fetchData(postId);
      };
    
      const handleClickParticipateCancel = async (registerId) => {
        await registerStore.cancelParticipateToGame(registerId);
        await fetchData(postId);
      };
    
      const acceptRegister = async (registerId) => {
        await registerStore.acceptRegister(registerId);
        await fetchData(postId);
      };
    
      const rejectRegister = async (registerId) => {
        await registerStore.rejectRegister(registerId);
        await fetchData(postId);
      };
    
      return (
        <Post
          // data props
          // handler props
        />
      );
    }

     

    Backend Application Layer

    API 요청 시, 화면에 접속한 사용자의 User Id가 Access Token으로 Header에 담겨 전달된다. Interceptor에서 decode된 User Id를 이용해 GameRepository에서 찾아온 Game에 사용자가 신청했거나 참가하는 상태가 있는지 판단한다. 이때 사용자가 취소하거나 거절당한 뒤 다시 신청한 경우 이전 기록도 같이 찾을 것이므로, 찾아온 상태들 중에서 신청 또는 참가 상태만을 filter한다. 화면에 접속한 사용자가 Register 상으로 어떤 상태인지 판별하는 Service의 동작은 다음과 같다.

     

    // GetGameService.java
    
    public class GetGameService {
        private final GameRepository gameRepository;
        private final RegisterRepository registerRepository;
    
        public GetGameService(GameRepository gameRepository,
                              RegisterRepository registerRepository) {
            this.gameRepository = gameRepository;
            this.registerRepository = registerRepository;
        }
    
        public GameDetailDto findTargetGame(Long accessedUserId,
                                            Long targetPostId) {
            Game game = gameRepository.findByPostId(targetPostId)
                .orElseThrow(GameNotFound::new);
    
    	// count number of participants
    
            Register myRegister = registerRepository
                .findAllByGameIdAndUserId(game.id(), accessedUserId)
                .stream()
                .filter(register -> register.status().value()
                    .equals(RegisterStatus.ACCEPTED)
                    || register.status().value()
                    .equals(RegisterStatus.PROCESSING))
                .findFirst().orElse(null);
    
            Long registerId = myRegister == null
                ? -1
                : myRegister.id();
    
            String registerStatus = myRegister == null
                ? "none"
                : switch (myRegister.status().value()) {
                case RegisterStatus.PROCESSING -> "processing";
                case RegisterStatus.ACCEPTED -> "accepted";
                default -> "none";
            };
    
            return new GameDetailDto(
                // informations of game,
                // number of participants,
                registerId,
                registerStatus
            );
        }
    }

     

     

    이슈

    해당 작업은 오후 3~4시를 전후로 마친 뒤, PATCH 동작을 구현해 실제로 신청/신청취소/참가취소/수락/거절 버튼을 눌렀을 때 동작이 처리되도록 하고, 최종적으로 간단한 CSS 처리를 해 최소한의 사용성을 줄 예정이었다. 문제는 오후 11시가 되어서야 최종적으로 오류 없이 의도대로 동작하도록 구현되었다. 작업이 길어진 원인으로 생각해본 점은 다음과 같다.

     

    • 총 작업 시간의 70% 가량을 테스트 코드를 작성하는 데 사용했다. 페이지 컴포넌트에서 Store의 상태와 함수를 모킹하는 과정에서 undefined 값을 가져오는 경우가 잦아 console.log로 소스코드 이곳저곳을 찾는 데 시간을 많이 사용했다.

    • UI를 정의하면서 만든 버튼의 핸들러 함수는 사실 PATCH 요청을 호출하는 함수의 핸들러 함수였기 때문에, 사실상 별도의 작업으로 나눴던 API 요청 작업을 같이 진행한 모양새가 되었다.

    • 백엔드 테스트 코드를 구현하는 과정에서 id를 자동으로 생성하는 구조로 만들었던 fakes 메서드를 사용하기가 어려워졌다. 모델들 간의 id의 차이로 도출되는 결과가 달라져야 했기에 여러 개의 파라미터를 전달하지 않고서는 fakes를 뜻대로 사용할 수가 없었는데, 그럴 바에 차라리 직접 데이터를 직접 생성자로 생성하기 시작하는 게 낫겠다는 생각이 들어 fake를 사용하던 지점의 구조를 변경했다. 한편 이로 인해 id 필드 역시 @EmbeddedId 어노테이션을 이용한 값 객체로 분리시켜 각 모델의 id 필드에도 의미를 부여해줄 필요성이 생겼다.

     

     

     

     

    댓글

Designed by Tistory.