[AndroidStudio, SpringBoot] KnockKnock 개발일지 -0124 (게시물, 댓글 조회 기능 구현)

Hyebin Lee·2022년 1월 24일
0

knockknock 개발일지

목록 보기
20/29

오늘의 목표

  1. ✔RecyclerView에서 각 게시물 클릭하면 게시글 상세 페이지로 이동하기 - 댓글도 조회
  2. ✔전체 게시글 조회 Activity 구현
  3. ✔전체 게시글 게시판 - 각 게시물 클릭시 상세 페이지+댓글 조회 (1번 중복)

참고한 링크

1. RecyclerView Click Event 처리 방법 2가지
2. 안드로이드 Intent로 액티비티 시작시 에러 android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag 해결

오늘의 이슈

  1. Ambiguous handler methods 이슈
  2. Retrofit 연결실패
  3. Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag.

오늘 개발의 핵심은 게시판에서 각 게시물을 터치했을 때 해당 게시물 페이지로 넘어가는 것 구현이였다.
일단 그것 외에도 자잘하게 원래 유저별 작성 게시글만 볼 수 있었던 것을 MainActivity에서 전체게시글을 조회할 수 있게 아래와 같이 구현했다.

여기서 각 게시글 아이템을 누르면 해당 상세 게시글 페이지로 넘어가 다음과 같이 보이게 되며 이때 게시글의 댓글까지 모두 보이게 된다.

자 그럼 한 번 만들어보자! 🧚‍♀️🧚‍♀️⭐


⭐ 상세 게시글 페이지 생성하기

1. PostList 조회받을 Data에 post PK(id)까지 받아오기

기존 HTTP 통신을 위한 PostData에는 작성자 닉네임, 제목, 내용, 작성일자와 시간만 통신하도록 설계했었다. 그러나 각 게시물의 id를 주고 받지 않으니까 나중에 해당 게시물의 댓글을 따로 조회하는 것을 구현하는 것이 힘들어졌다. 그래서 미리 게시판 형태에서의 조회에서도 각 게시물마다 pk를 받아놓고, 각 게시물 터치시 해당 게시물 상세 페이지로 이동해서 해당 게시물과 물려있는 댓글들까지도 조회하도록 수정했다.


🔺안드로이드에서 받을 PostData에 게시물의 id값 매칭

🔺서버에서 안드로이드에게 줄 PostSaveResponse 클래스에 id 값 추가

2. 상세 게시글 페이지 레이아웃(xml) 생성

이제 게시판에서 각 게시물을 클릭했을 때 넘어가는 게시글 상세 페이지 레이아웃을 생성한다!
여기서의 핵심은 댓글까지 확인할 수 있게 하는 것이다.


일단은 🤎디자인이지만 데이터를 잘 가져와서 찍어내는게 목표이니까 이정도만 구현해보았다.
댓글 부분은 RecyclerView로 구현해서 이제 그 Recycler의 댓글 아이템 viewLayout도 제작해주어야 한다.

3. 댓글 아이템 viewLayout(xml) 생성


대충 이렇게 만들었다! 이제 기본적인 layout 틀을 만드는데는 조금 익숙해진 것 같다(만드는 속도가 빨라졌다) 희희
참고로 recyclerview에 들어갈 view아이템을 생성할 때는

  • view 아이템이 어떤 식으로 정렬될 것인지에 맞춰서 가장 상위의 linerlayout을 설정하는 것이 좋다.
    : 나는 댓글을 아래로 쌓아둘 것이기 때문에 vertical로 설정하였다.
  • 또한 댓글 박스는 한 페이지에 여러개 생성되야 하기 때문에 이후에 만든 댓글 박스에 대해서는 꼭 height를 사용자설정으로 값을 줘야한다. 경험상 100dp정도가 적당한 것 같다.

4. 서버에서 api 구현하기

아.. 이 부분은 구현할 것들이 많았다. 일단 각 게시물 하나 조회에 대한 api가 서버 측에서도 개발이 안되어있었다. 그래서 각 게시물 하나 조회에 대한 api를 구현하고 그에 대한 response dto를 각 게시물과 댓글까지 따로 구현해야 했다. 바쁘다바빠

  //게시글 하나 detail 정보 조회
    @GetMapping("api/post/view/{postId}")
    public PostViewResponse viewEachPost(@PathVariable("postId")Long id){
        Post post = postService.getPostById(id);
        PostViewResponse response =  new PostViewResponse(post.getId(),
               post.getPostwriter().getNickName(),
                post.getTitle(),
                post.getContent(),
                post.getTimestamp());
        List<Comment> comments = post.getPostcomments();
        List<CommentDto> commentDtoList = comments.stream().map(c->new CommentDto(c.getId(),c.getCommentwriter().getNickName(),c.getTimestamp(),c.getContent()))
                .collect(Collectors.toList());
        response.setCommentlist(commentDtoList);
        return response;
    }

어찌저찌 구현을 위와 같이 마치고 이제 POSTMAN에서 테스트를 해보려는데 아래와 같은 오류가 났다.

⛔Ambiguous handler methods 이슈

java.lang.IllegalStateException: Ambiguous handler methods mapped for '/api/post/view/2': {public jpaproject.knockknock.api.response.PostViewResponse jpaproject.knockknock.api.PostController.viewEachPost(java.lang.Long), public jpaproject.knockknock.api.PostController$Result jpaproject.knockknock.api.PostController.viewPostofWriter(java.lang.String)}
	at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:432)
	at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:383)
	at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:125)
	at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:67)
	at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:498)
	at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1261)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1043)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:655)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:764)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1722)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:834)

구글링해보니 이 이슈는 같은url이 중복되어 존재한다는 이야기였다.
찾아보니 실제로 같은 url이 중복으로 쓰이고 있었다 ㅠㅠㅠㅠ api url 경로 짓는게 은근 어렵다ㅎ...
개발은 역시 작명이 제일 어렵다더니

우여곡절 끝에 서버쪽 개발을 모두 완료했다.

5. 안드로이드에서 RecyclerView를 위한 Retrofit 구현하기

  • Data class 만들기
    위의 json 형식 response를 바탕으로 CommentData와 CommentData를 리스트로 갖는 PostDetailData 클래스를 구현했다.

  • api interface 만들기

 @GET("api/post/view/{postId}")
    Call<PostDetailData> getPostDatabyPostId(@Path("postId")Long id);

이제는 API 구현이 제일 쉽다(객체지향, 코드 효율 고려 1도 안했을 때 ㅎ)

6. ⭐게시판 리스트 RecyclerView에서 Click Event 처리

  • 먼저 ItemClickListener 인터페이스를 하나 생성한다.
package org.techtown.knockknock;

import android.view.View;

public interface ItemClickListener {
    void onItemClickListener(View v, int position);
}

  • RecyclerView Adapter (게시판 recyclerview)에서 click 이벤트 처리 코드 작성

Adapter의 Holder부분

Holder에서 ItemClickListner 객체를 생성하고 홀더에서 생성할 listView에 onClickListener설정을 해준다. 이후 onClick 메소드에서 itemClickListener로 해당 view와 layoutposition을 객체로 메소드 설정해준다.

//Holder: 레이아웃과 연결해서 listView를 만들어주는 역할 (단순 연결)
    public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener{

        TextView title;
        TextView content;
        TextView date;

        ItemClickListener itemClickListener; //📌

        public MyViewHolder(@NonNull View itemView) {
            super(itemView);

            title = (TextView)itemView.findViewById(R.id.tv_postlist_title);
            content = (TextView)itemView.findViewById(R.id.tv_postlist_content);
            date = (TextView)itemView.findViewById(R.id.tv_postlist_timestamp);

            itemView.setOnClickListener(this); //📌
        }
        @Override //📌
        public void onClick(View v){
            this.itemClickListener.onItemClickListener(v,getLayoutPosition());
        }
    }

Adapter의 onBindViewHolder 메소드 부분

holder의 itemClickListener 변수를 설정해준다. 새 ItemClickListener 객체를 만들고 onItemClickListener 메소드 오버라이드에서 구현하고 싶은 내용을 구현하면 된다.
참고로 position은 list 형태로 담겨진 데이터의 index이기 때문에 list형태로 담겨진 데이터 중 하나의 정보에 접근할때는 list.get(position).getXX(); 형태로 접근하면 된다
내가 구현한 내용은 접근할 게시글 하나의 아이디를 가져와서 PostdetailActivity로 intent를 사용해서 페이지 넘어가며 그때 같이 해당 게시글의 아이디를 가져가 새로 넘어간 PostdetailActivity에서 그 아이디로 새롭게 게시글의 detail한 데이터를 조회하여 출력해주는 시나리오로 구현했다.

@Override
public void onBindViewHolder(@NonNull RecyclerAdapter.MyViewHolder holder, int position){
    holder.title.setText(postlist.get(position).getTitle());
    holder.date.setText(postlist.get(position).getDate());
    holder.content.setText(postlist.get(position).getContent());


    holder.itemClickListener = new ItemClickListener() { //📌
        @Override //📌
        public void onItemClickListener(View v, int position) {

            Long postId = postlist.get(position).getId();
            Intent intent = new Intent(v.getContext(),PostdetailActivity.class);
            intent.putExtra("postid",postId);
            v.getContext().startActivity(intent);
        }
    };
}

7. PostdetailActivity 구현

코드는 많지만 계속해서 Retrofit 구현 + RecyclerView 활용 부분이 반복되어서 구체적인 설명은 생략하겠다! 이제는 코드만 봐도 아아 이해할 수 있을 정도가 되었기를..! (아닌가😧)
여기서의 핵심은 📌 핀꽂은 부분! Intent로 넘어왔으니 같이 넘어온 post의 id를 가지고 새롭게 조회해서 관련 데이터를 출력하는 것이다.

package org.techtown.knockknock.post;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.google.gson.Gson;

import org.techtown.knockknock.ErrorBody;
import org.techtown.knockknock.R;
import org.techtown.knockknock.RetrofitClient;

import java.util.ArrayList;
import java.util.List;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class PostdetailActivity extends AppCompatActivity {

    TextView title;
    TextView writer;
    TextView timestamp;
    TextView content;
    List<CommentData> comments;

    RecyclerView recyclerView;
    CommentRecyclerAdapter recyclerAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_postdetail);

        comments = new ArrayList<>();
        recyclerView = findViewById(R.id.recyclerView_postdetail);

        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);

	//📌
        Intent intent = getIntent();
        Long postid = intent.getExtras().getLong("postid");
        
        PostAPI postAPI = RetrofitClient.getInstance().create(PostAPI.class);
        Call<PostDetailData> call = postAPI.getPostDatabyPostId(postid);
        call.enqueue(new Callback<PostDetailData>() {
            @Override
            public void onResponse(Call<PostDetailData> call, Response<PostDetailData> response) {
                if(response.isSuccessful()){

                    PostDetailData postdata = response.body();

                    title = findViewById(R.id.tv_postdetail_title);
                    writer = findViewById(R.id.tv_postdetail_writer);
                    timestamp = findViewById(R.id.tv_postdetail_timestamp);
                    content = findViewById(R.id.tv_postdetail_content);

                    title.setText(postdata.getPostTitle());
                    content.setText(postdata.getPostContent());
                    writer.setText(postdata.getPostwriter());
                    timestamp.setText(postdata.getPostedTime());
                    comments = postdata.comments;

                    recyclerAdapter = new CommentRecyclerAdapter(getApplicationContext(),comments);
                    recyclerView.setAdapter(recyclerAdapter);


                }
                else{
                    ErrorBody error = new Gson().fromJson(response.errorBody().charStream(),ErrorBody.class);
                    Log.d("PostdetailActivity",error.getMessage());
                }
            }

            @Override
            public void onFailure(Call<PostDetailData> call, Throwable t) {
                Log.d("PostdetailActivity","게시글 조회 연결 실패");

            }
        });
    }
}

Retrofit 연결실패


갑자기 잘되던 retrofit이 돌연 안되는 불상사가 생겼다.
코드 건들지도 않았는데 기존의 login 페이지부터 로그인이 안되어버리고 연결 실패라니까 미쳐버리는줄 알았다.
열심히 구글링해도 뾰족한 수가 나오지 않고 있었는데..
검색해보니 테스트하는 기기랑 인터넷 ip가 다르면 그럴 수 있다고 한다.
지금 토즈에서 인터넷 ip를 받아 쓰고 있는게 그게 랜덤으로 기기마다 다르게 지급되는 것인지.. 왜 안되는지 이해가 안가지만 이 문제때문에 맞는거 같은게 가상머신으로 안드로이드를 돌리면 또 통신이 잘 된다.
잘 모르겠지만 일단 가상머신으로 돌리는 걸로....😢

Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag.

게시판 목록에서 각 게시물 항목을 클릭해서 detail한 게시글 Activity로 이동하려고 시도한 순간 다음과 같은 오류가 나면서 어플이 종료되어 버렸다.

2022-01-24 15:45:17.173 14232-14232/? E/AndroidRuntime: FATAL EXCEPTION: main
   Process: org.techtown.knockknock, PID: 14232
   android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
       at android.app.ContextImpl.startActivity(ContextImpl.java:952)
       at android.app.ContextImpl.startActivity(ContextImpl.java:928)
       at android.content.ContextWrapper.startActivity(ContextWrapper.java:383)
       at org.techtown.knockknock.post.RecyclerAdapter$1.onItemClickListener(RecyclerAdapter.java:57)
       at org.techtown.knockknock.post.RecyclerAdapter$MyViewHolder.onClick(RecyclerAdapter.java:88)
       at android.view.View.performClick(View.java:7125)
       at android.view.View.performClickInternal(View.java:7102)
       at android.view.View.access$3500(View.java:801)
       at android.view.View$PerformClick.run(View.java:27336)
       at android.os.Handler.handleCallback(Handler.java:883)
       at android.os.Handler.dispatchMessage(Handler.java:100)
       at android.os.Looper.loop(Looper.java:214)
       at android.app.ActivityThread.main(ActivityThread.java:7356)
       at java.lang.reflect.Method.invoke(Native Method)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

이 문제는 Activity가 아닌 곳에서 startActivity를 하려니까 터지는 문제였다.
실제로 문제가 생긴 부분은 내가 RecyclerAdapter에서 itemView 클릭되면 해당 게시글의 detail한 activity로 intent써서 이동하려고 하는 부분이였다.
이 문제는 다행히 간단하게 해결할 수 있었는데, 원래 그냥 인풋으로 intent만 줬던 부분을
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))로 주면 된다.

v.getContext().startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));

하 드디어 게시판 게시글 목록에서 게시글 하나 클릭하면 게시글의 디테일한 화면 출력과 해당 게시글의 댓글까지 조회되는 서비스를 구현했다!!!!! 🤸‍♀️💃🎇🎆

물론 게시글의 이미지, 해시태그처리, UI 다듬기, 자잘한 정보 수정 등등 손봐야할 부분이 많지만 일단 큰 틀에서는 큰 발전을 했다 하하 행복해

전체 게시글 조회와 각 게시글로 이동

이 부분은 사실 여태까지 구현한 부분과 많이 겹쳐서 별 무리 없이 빠르게 구현했다!
특히 결국 RecyclerView에서 각 게시글 viewItem 형태는 앞에서 제작했던 형태와 동일해서 Holder와 Adapter를 건들일게 없었고 마찬가지로 각 게시글로 이동도 똑같기 때문에
그냥 바뀌는거 하나 없이 API만 전체 게시글 조회 API하나 더 만들고 해당 API로 조회하도록 했다! 나머지는 다 똑같다 야호

@GET("api/post/view")
   Call<PostListData> getPostList();

0개의 댓글