묻고 답해요
141만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결[초급편] 안드로이드 커뮤니티 앱 만들기(Android Kotlin)
댓글 오류 관련해 질문드립니다.
안녕하세요 선생님, 우선 강의 내용과 직접적으로 관련된 질문이 아니라 죄송합니다. 댓글 관련 오류가 몇 가지 발생해 여쭤봅니다.BoardReadActivity.kt 생략 // 댓글 목록 정보 가져옴 fun getCommentListData(key: String) { 생략 // 댓글 헤더에 댓글 개수 출력 binding.commentCountText.text = commentKeyList.count().toString() 생략 }기존 댓글 2개가 있는 상황에서 새 댓글을 1개 입력하면 댓글 개수가 3이 되어야 하는데요,2(기존) + 3(기존2업뎃1) = 5(누적)로 출력이 됩니다.또한 방금 막 작성한 댓글 a의 수정 페이지로 넘어가면추가된 댓글이 즉시 반영되지 않아 맨 위 댓글인 user1이 불러와지며댓글 a가 아닌 맨 위 댓글이 수정됩니다.마찬가지로 a를 삭제하려해도 user-1이 불러와지며삭제되는 댓글도 a가 아닌 user-1입니다.수정과 삭제를 반복하면 댓글 카운트는 계속 늘어나고, 이 과정에서 앱이 죽기도 합니다.게시글을 한 번 나갔다 들어와야 댓글 수가 제대로 카운트 돼서 어떻게 해야 할지 도움 요청드립니다. 코드 전체는 깃허브에 업로드 해 놓았습니다. https://github.com/shinyelee/my-solo-life
-
해결됨[초급편] 안드로이드 커뮤니티 앱 만들기(Android Kotlin)
댓글 수정 및 삭제 구현 관련해 질문드립니다
안녕하세요 선생님, [초급편] 안드로이드 커뮤니티 앱 만들기를 완강한 수강생입니다. 게시글 수정/삭제 파트와 아래 두 질문을 참고해가며 현재 댓글 수정/삭제를 구현하기 위해 애쓰는 중인데, 잘 되지 않아 질문드립니다.https://www.inflearn.com/questions/411607https://www.inflearn.com/questions/619233게시글 설정과 마찬가지로 내가 쓴 댓글에만 보이는 설정 버튼을 클릭하면다이얼로그를 통해 수정/삭제로 진입하게끔 만드는 데 까지는 성공했습니다.첫 번째 질문입니다.우선 수정 기능의 경우 아래 ????? 부분에 어떤 값을 넣어야 할지 감이 안 옵니다. BoardReadActivity.kt // 댓글 클릭하면 -> 대화상자 뜸 // 파이어베이스의 댓글 키를 기반으로 댓글 데이터(=본문+uid+시간) 받아옴 cLV.setOnItemClickListener { parent, view, position, id -> // 명시적 인텐트 -> 다른 액티비티 호출 val intent = Intent(baseContext, CommentEditActivity::class.java) // 댓글수정 액티비티로 댓글의 키 값 전달 intent.putExtra("?????", ?????) // 댓글수정 액티비티 시작 startActivity(intent) }"key", key를 넣으면 댓글이 아닌 게시글의 키 값이 되어 댓글이 수정되지 않고,"commentKey", commentKey를 넣으면 앱이 죽습니다. 게시판 프래그먼트에서 리스트뷰 아이템을 클릭하면 게시글로 이동하듯댓글 리스트뷰 아이템을 클릭하면 수정 액티비티로 넘어가는 것도 시도해봤는데요, 빨간색이 게시글의 키, 파란색이 댓글의 키일 때 다행히 댓글의 키는 잘 받아오는데 댓글의 키'만' 알다보니 파이어베이스로부터 기존 댓글 내용을 받아올 수도, 수정한 내용을 업데이트 할 수도 없습니다.BoardReadActivity.kt // 댓글 클릭하면 -> 대화상자 뜸 // 파이어베이스의 댓글 키를 기반으로 댓글 데이터(=본문+uid+시간) 받아옴 cLV.setOnItemClickListener { parent, view, position, id -> // 명시적 인텐트 -> 다른 액티비티 호출 val intent = Intent(baseContext, CommentEditActivity::class.java) // 댓글수정 액티비티로 댓글의 키 값 전달 intent.putExtra("commentKey", commentKeyList[position]) // 댓글수정 액티비티 시작 startActivity(intent) }다이얼로그에서 댓글수정 액티비티로 넘길 때와 마찬가지로 ????? 부분에 어떤 값을 넣어야 할지 모르겠습니다.CommentEditActivity.kt // 댓글을 수정 private fun editCommentData(commentKey: String) { // 수정한 값으로 업데이트 FBRef.commentRef.?????.child(commentKey).setValue(CommentModel( // 제목 및 본문은 직접 수정한 내용으로, binding.commentMainArea.text.toString(), // uid와 시간은 자동 설정됨 FBAuth.getUid(), FBAuth.getTime() )) // 수정 확인 메시지 Toast.makeText(this, "댓글이 수정되었습니다", Toast.LENGTH_SHORT).show() // 댓글수정 액티비티 종료 finish() }혼자 해결하려니 너무 맨 땅에 헤딩이라ㅠㅠ 질문 올립니다.
-
해결됨[초급편] 안드로이드 커뮤니티 앱 만들기(Android Kotlin)
댓글 출력 관련해 질문드립니다
안녕하세요 선생님, 현재 [초급편] 안드로이드 커뮤니티 앱 만들기 수강 중 섹션 8. 게시판 댓글 만들기의 댓글 불러오기까지 진행한 상태입니다. 댓글 리스트뷰를 강의와 조금 다르게 출력하고 싶은데, 난관에 부딪혀 질문드립니다. 현재 강의에서처럼 리스트뷰에 height를 dp 단위로 지정하면 height를 충분하게 설정했을 때엔 여백이 많이 생기고,height가 부족하면 댓글이 잘리는 문제가 생기더라고요. 저는 위 캡쳐처럼 여백이 없고, 댓글란이 잘리지 않고, 이중스크롤이 필요없는 댓글 영역을 만들고 싶습니다. 여러 방법을 알아봤는데 캡쳐처럼 아이템 1개 height 내에서 댓글란만 따로 스크롤을 가능하게 하거나, 화면을 2분할해 본문만 스크롤/ 댓글만 스크롤 이런 식으로 처리하는 게 차선책이더라고요. 여태까지 https://stackoverflow.com/questions/6210895/listview-inside-scrollview-is-not-scrolling-on-android/11554684#11554684 https://trivedihardik.wordpress.com/2011/09/19/scrollview-inside-scrollview-scrolling-problem/ https://hoonkim1126.tistory.com/5 https://itstudentstudy.tistory.com/49 정도를 시도해봤는데 대부분 오래되고 자바로 쓰인 코드라 제대로 작동하지 않아 선생님께 도움을 요청드립니다. 댓글을 리사이클러뷰로 아예 갈아엎는 건 최후의 보루로 남겨놓고 있습니다ㅠㅠ
-
미해결파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트
안녕하세요 선생님 DRF 대댓글에 대해서 질문드립니다!!
안녕하세요 선생님 DRF 대댓글에 대해서 질문드리려고 합니다. 제가 원하는 대댓글 구현은 계층형인데요 (물론 요즘은 잘 안쓰는걸로 알고있지만;;) 예를 들어 (reddit이랑 댓글 구현이 정확히 일치합니다!! -> 계층형 댓글로 구현이 되있습니다) 댓글 -대댓글 --대대댓글 --대대댓글 -대댓글 --대대댓글 댓글 -대댓글 -대댓글 댓글 ... 위와 같이 어떠한 포스트글에 댓글, 대댓글 전부 한번에 보이게 구현을 하고 싶은데 보통 대댓글도 결국 댓글이라서 self join하자나요? class Comment(TimestampedModel): author = models.ForeignKey(User, on_delete=models.CASCADE) post = models.ForeignKey(Post, on_delete=models.CASCADE, verbose_name='포스트 제목') message = models.TextField(verbose_name='댓글') parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True) 근데 문제는 pagination이 걸려 있다보니깐 어떠한 댓글에 대댓글, 대대댓글을 저렇게 정렬된 상태로 한번에 가져오는 방법이 떠오르지가 않네요; (댓글에 좋아요도 추가해서 정렬 알고리즘을 추가할 예정입니다) 그래서.. 1. pagination을 풀어서 어떠한 포스트에 댓글을 전부 response하고 클라이언트 단에서 정렬하라고 하는건지? (물론 당연히 DB 부담이 커서 처음엔 이 방법을 전혀 생각하고 있지 않았지만, 한편으로는 댓글은 많아봐야 몇천개라서 커버가 될 수있을까 ? 라는 막연한 생각이드네용) 2. 댓글 하나당 대댓글이 있는지 재귀적으로 요청해서 대댓글이 있으면 가져오고 없으면 넘어가는 식으로 구현? (물론 당연히 이것도 하나의 포스트에 여러번의 DB 요청을 하기 때문에 서버측에서 굉장히 부담이 커서 현실성이 매우 떨어짐) 위 두 방법은 좀 현실성이 떨어져서 좀 더 효과적으로 구현할 수 있는 방법이 있을것같은데 실력과 경험부족으로 떠오르지가 않네요... 혹시 위 처럼 계층형 대댓글을 구현하는데 좋은 로직이나 자료나 솔루션이 있을까요???? (프론트엔드는 코틀린입니다)
-
해결됨남박사의 파이썬으로 실전 웹사이트 만들기
(게시판 댓글구현 오류) count 부분에서 오류가 나는데.. 어떻게 해야할까요..ㅠ?
안녕하세요 남박사님 남박사님 덕분에 강의를 잘듣고 진도를 나가고 있습니다. 이해가 안가는 부분도 아직 있지만 열심히 배울려고 노력하고 있는데요! 이번 댓글기능 만들기 부분에서 오류나는 부분있어서 질문올립니다. 댓글기능 구연하고 마지막 부분에서 jinja2.exceptions.UndefinedError: 'pymongo.cursor.Cursor object' has no attribute 'count'라고 오류가 나는데.. 저번에 {% if comments.count() > 0 %} 이부분이 몽고db 부분하고 연동이 안되는거 같아서 버전 차이라고 하셔서 count_documents({조건})로 변경해봤는데.. 오류는 나더라구요..! - 오류 - -코드 - -board.py- from main import * from flask import Blueprint, send_from_directory from flask import send_from_directory bluerprint = Blueprint("board", __name__, url_prefix="/board") def board_delete_attach_file(filename): abs_path = os.path.join(app.config["BOARD_ATTACH_FILE_PATH"], filename) if os.path.exists(abs_path): os.remove(abs_path) return True return False @bluerprint.route("/comment_write", methods=["POST"]) @login_required def comment_write(): if request.method == "POST": print("POST OK") name = session.get("name") writer_id = session.get("id") root_idx = request.form.get("root_idx") comment = request.form.get("comment") current_utc_time = round(datetime.utcnow().timestamp() * 1000) c_comment = mongo.db.comment post = { "root_idx": str(root_idx), "writer_id": writer_id, "name": name, "comment": comment, "pubdate": current_utc_time } print(post) c_comment.insert_one(post) return redirect(url_for("board.board_view", idx=root_idx)) @bluerprint.route("/upload_image", methods=["POST"]) def upload_image(): if request.method == "POST": file = request.files["image"] if file and allowed_file(file.filename): filename = "{}.jpg".format(rand_generator()) savefilepath = os.path.join(app.config["BOARD_IMAGE_PATH"], filename) file.save(savefilepath) return url_for("board.board_images", filename=filename) @bluerprint.route("/images/<filename>") def board_images(filename): return send_from_directory(app.config["BOARD_IMAGE_PATH"], filename) @bluerprint.route("/files/<filename>") def board_files(filename): return send_from_directory(app.config["BOARD_ATTACH_FILE_PATH"], filename, as_attachment=True) @bluerprint.route("/list") def lists(): # 페이지 값 (값이 없는 경우 기본값는 1) page = request.args.get("page", 1, type=int) # 한페이지당 몇개의 게시물을 출력할지 limit = request.args.get("limit", 5, type=int) search = request.args.get("search", -1, type=int) keyword = request.args.get("keyword", "", type=str) # 최종적으로 완성된 쿼리를 만들 변수 query = {} # 검색어 상태를 추가할 리스트 변수 search_list = [] if search == 0: search_list.append({"title": {"$regex": keyword}}) elif search == 1: search_list.append({"contents": {"$regex": keyword}}) elif search == 2: search_list.append({"title": {"$regex": keyword}}) search_list.append({"contents": {"$regex": keyword}}) elif search == 3: search_list.append({"name": {"$regex": keyword}}) # 검색 대상이 한개라도 존재할 경우 query 변수에 $or 리스트를 쿼리 합니다. if len(search_list) > 0: query = {"$or": search_list} print(query) board = mongo.db.board datas = board.find({}).skip( (page - 1) * limit).limit(limit).sort("pubdate", -1) # 게시물의 총 갯수 tot_count = board.count_documents({}) # 마지막 페이지의 수를 구한다. last_page_num = math.ceil(tot_count / limit) # 페이지 블럭을 5개씩 표기 block_size = 5 # 현재 블럭의 위치 block_num = int((page - 1) / block_size) # 블럭의 시작 위치 block_start = int((block_size * block_num) + 1) # 블럭의 끝 위치 block_last = math.ceil(block_start + (block_size - 1)) return render_template( "list.html", datas=list(datas), limit=limit, page=page, block_start=block_start, block_last=block_last, last_page_num=last_page_num, search=search, keyword=keyword, title="게시판 리스트") @bluerprint.route("/view/<idx>") @login_required def board_view(idx): # idx = request.args.get("idx") if idx is not None: page = request.args.get("page") search = request.args.get("search") keyword = request.args.get("keyword") board = mongo.db.board # data = board.find_one({"_id": ObjectId(idx)}) data = board.find_one_and_update({"_id": ObjectId(idx)}, { "$inc": {"view": 1}}, return_document=True) if data is not None: result = { "id": data.get("_id"), "name": data.get("name"), "title": data.get("title"), "contents": data.get("contents"), "pubdate": data.get("pubdate"), "view": data.get("view"), "writer_id": data.get("writer_id", ""), "attachfile": data.get("attachfile", "") } comment = mongo.db.comment comments = comment.find({"root.idx": str(data.get("_id"))}) return render_template("view.html", result=result, comments=comments, page=page, search=search, keyword=keyword, title="글 상세보기") return abort(404) @bluerprint.route("/write", methods=["GET", "POST"]) def board_write(): if request.method == "POST": filename = None if "attachfile" in request.files: file = request.files["attachfile"] if file and allowed_file(file.filename): filename = check_filename(file.filename) file.save(os.path.join(app.config['BOARD_ATTACH_FILE_PATH'], filename)) name = request.form.get("name") title = request.form.get("title") contents = request.form.get("contents") request.files current_utc_time = round(datetime.utcnow().timestamp() * 1000) board = mongo.db.board post = { "name": name, "title": title, "contents": contents, "pubdate": current_utc_time, "writer_id": session.get("id"), "view": 0, } if filename is not None: post["attachfile"] = filename x = board.insert_one(post) print(x.inserted_id) return redirect(url_for("board.board_view", idx=x.inserted_id)) else: return render_template("write.html", title="글 작성") @bluerprint.route("/edit/<idx>", methods=["GET", "POST"]) def board_edit(idx): if request.method == "GET": board = mongo.db.board data = board.find_one({"_id": ObjectId(idx)}) if data is None: flash("해당 게시물이 존재하지 않습니다.") return redirect(url_for("board.lists")) else: if session.get("id") == data.get("writer_id"): return render_template("edit.html", data=data, title="글 수정") else: flash("글 수정 권한이 없습니다.") return redirect(url_for("board.lists")) else: title = request.form.get("title") contains = request.form.get("contents") deleteoldfile = request.form.get("deleteoldfile", "") board = mongo.db.board data = board.find_one({"_id": ObjectId(idx)}) if session.get("id") == data.get("writer_id"): filename = None if "attachfile" in request.files: file = request.files["attachfile"] if file and allowed_file(file.filename): filename = check_filename(file.filename) file.save(os.path.join(app.config["BOARD_ATTACH_FILE_PATH"], filename)) if data.get("attachfile"): board_delete_attach_file(data.get("attachfile")) else: if deleteoldfile == "on": filename = None if data.get("attachfile"): board_delete_attach_file(data.get("attachfile")) else: filename = data.get("attachfile") board.update_one({"_id": ObjectId(idx)}, { "$set": { "title": title, "contents": contains, "attachfile": filename } }) flash("수정 되었습니다.") return redirect(url_for("board.board_view", idx=idx)) else: flash("글 수정 권한이 없습니다.") return redirect(url_for("board.lists")) @bluerprint.route("/delete/<idx>") def board_delete(idx): board = mongo.db.board data = board.find_one({"_id": ObjectId(idx)}) if data.get("writer_id") == session.get("id"): board.delete_one({"_id": ObjectId(idx)}) flash("삭제 되었습니다.") else: flash("삭제 권한이 없습니다.") return redirect(url_for("board.lists")) ------------------------------------------------------------------------------------------------------- - view.html- {% extends "main.html" %} {% block contents %} <div style="padding: 50px 50px 50px 50px;"> <table class="table table-bordered"> <tbody> <tr> <td colspan="2">{{result.title}}</td> </tr> <tr> <td>{{result.name}}</td> <td class="text-right">{{result.pubdate|formatdatetime}}</td> </tr> {% if result.attachfile %} <tr> <td>첨부파일</td> <td><a href="{{url_for('board.board_files', filename=result.attachfile)}}">{{result.attachfile}}</a></td> </tr> {% endif %} <tr> <td colspan="2"><div style="min-height: 200px;"></div>{% autoescape false %}{{result.contents}}{% endautoescape %}</td> </tr> </tbody> </table> <a class="btn btn-primary" href="{{url_for('board.lists', page=page, search=search, keyword=keyword)}}">리스트</a> {% if session["id"] == result.writer_id %} <a class="btn btn-danger float-right ml-2" href="{{url_for('board.board_delete', idx=result.id)}}">글삭제</a> <a class="btn btn-warning float-right" href="{{url_for('board.board_edit', idx=result.id)}}">글수정</a> {% endif %} <br> <br> <form id="commentForm" name="commentForm" action="{{url_for('board.comment_write')}}" method="POST"> <input type="hidden" name="csrf_token" value="{{csrf_token()}}"> <input type="hidden" name="root_idx" value="{{result.id}}"> <div> <span><strong>댓글</strong></span> <span id="cCnt"></span> <table class="table"> <tr> <td><textarea rows="3" cols="110" id="comment" name="comment" placeholder="댓글을 입력하세요"></textarea></td> <td><input type="submit" class="btn btn-success" style="height: 80px;" value="등록하기"></td> </tr> </table> </div> </form> {% if comments.count() > 0 %} {% for c in comments %} <div> <table class="table"> <tr> <td width="100"><h6>{{c.name}}</h6></td> <td>{{c.comment}}</td> <td class="text-right" width="200">{{c.pubdate | formatdatetime}}</td> </tr> </table> </div> {% endfor %} {% endif %} </div> {% endblock %}