사용자:Senouis/SectionRatings 확장 기능 계획: 두 판 사이의 차이

리버티게임(개발), 모두가 만들어가는 자유로운 게임
둘러보기로 이동 검색으로 이동
잔글 (→‎공통)
 
(같은 사용자의 중간 판 25개는 보이지 않습니다)
3번째 줄: 3번째 줄:
게임별 토론 문서의 게임 평가용 문단 내용을 바탕으로 SQL 쿼리를 사용해 이 게임의 평점이 리버티게임 전체에서 상위 몇 퍼센트인지 알려주는 SectionRatings(가칭)의 설계에 관한 문서입니다.
게임별 토론 문서의 게임 평가용 문단 내용을 바탕으로 SQL 쿼리를 사용해 이 게임의 평점이 리버티게임 전체에서 상위 몇 퍼센트인지 알려주는 SectionRatings(가칭)의 설계에 관한 문서입니다.


* [https://github.com/Xen-alpha/libertygame-extension-sectionratings GitHub Repo]
* TODO: UML의 문법에 따라 클래스 다이어그램, 상태 다이어그램(FSM도 좋지만 가능하면 순서도로), 데이터 흐름 명시할 것
== 요구 사항 ==
== 요구 사항 ==
* [[본진:사용자:Hsl0/DiscussionTools|기존 요구 사항 자료]]: 대량 편집으로 평가 문단을 다음과 같이 바꾸는 것을 제안
* [[본진:사용자:Hsl0/DiscussionTools|기존 요구 사항 자료]]: 대량 편집으로 평가 문단을 다음과 같이 바꾸는 것을 제안
21번째 줄: 24번째 줄:
* 클라이언트 측에서 토론 문서를 보았을 때 평론 틀이 풀기 처리된 토론 문서의 HTML element 계층 구조는 같은 level을 가지고 있어 클라이언트 측 파싱은 매우 복잡하거나 불가능할 가능성이 큼(h2, h3, ol 태그가 전부 같은 부모를 가짐)
* 클라이언트 측에서 토론 문서를 보았을 때 평론 틀이 풀기 처리된 토론 문서의 HTML element 계층 구조는 같은 level을 가지고 있어 클라이언트 측 파싱은 매우 복잡하거나 불가능할 가능성이 큼(h2, h3, ol 태그가 전부 같은 부모를 가짐)
* 자체 개발이 좋으나 PHP 확장 기능 개발의 어려움으로 인해 실패할 가능성이 크므로 두 가지 방법을 고려<s>새삼 조사 확장 기능을 개발한 *devunt님이 대단해지는 순간</s>
* 자체 개발이 좋으나 PHP 확장 기능 개발의 어려움으로 인해 실패할 가능성이 크므로 두 가지 방법을 고려<s>새삼 조사 확장 기능을 개발한 *devunt님이 대단해지는 순간</s>
=== 공통 ===
=== 백엔드 개발 공통 명세 ===
* [[mw:API:REST API/Extensions]]의 내용에 따라 REST API를 작성하여 다음과 같은 URL로 평가 요청(첫 평가는 PUT, 변경 시 POST)하도록 할 것
<s>
** 예: dev.libertygame.work/rest.php/sectionratings/v0/rategame/(게임 이름)/(별점)
* [[mw:API:REST API/Extensions]]의 내용에 따라 REST API를 작성하여 다음과 같은 URL로 평가 요청(첫 평가는 PUT, 변경 시 POST)하도록 할 것: (예정)
* 평가 취합은 GET 요청으로 다음 REST API 작성
** 예: dev.libertygame.work/rest.php/sectionratings/v0/rategame/(게임 이름:문자열)/(별점:정수)
** 예: dev.libertygame.work/rest.php/sectionratings/v0/getgameratings/(게임 이름)
** 반환 결과인 JSON 객체는 "Success"/"Fail" 같은 값으로 표시한 평가 성공 여부를 포함해야 함</s>
* 평가 취합은 GET 요청으로 다음 REST API 작성: {{완료}}
** 예: dev.libertygame.work/rest.php/sectionratings/v0/ratings/(분류:문자열)/(반환할 갯수:정수)
** 반환값은 현재 게임 순위, 평가자 수, 평점을 포함: 예 - https://dev.libertygame.work/rest.php/sectionratings/v0/ratings/분류:철도_교통_게임/4
*** 현재 게임 순위는 평점이 소수점 아래 한 자리까지 동일할 경우 평가자 수가 많을 수록 작은 숫자의 순위(높은 순위)를 가짐
 
=== <s>프론트엔드 개발 명세</s> {{거부|Authentication 문제로 인해 SQL 파일 작성으로 대체됨}} ===
* [[미디어위키:Gadget-SectionRatings.js|도구 스크립트]] - [[미디어위키:Common.js|커먼자스]]로 아래 내용에 해당하는 코드를 옮길 것
* 최대 4개를 가져오도록 요청한 다음 백엔드에서 가져온 데이터를 바탕으로 [[틀:게임카드]]에 적힌 대로 ','(쉼표)로 구분한 게임카드 파라미터를 구성
** 그 다음 action=parse 미디어위키 API로 [[미디어위키:Gadget-gamelistjs.js]]와 유사하게<ref>단, 문서 제목을 넘기지 않고 내용을 직접 파라미터로 렌더링해야 함</ref> 게임카드 틀에 대한 렌더링을 요청하는 Promise를 작성하여 스크립트로 추가
{{#mermaid:flowchart LR
  id1["client"] <--> |1:getgameratings REST API| id2["rest.php"]
  id1["client"] <--> |3:parse API with GameCard Template| id3["api.php"]
  id2["rest.php"] <--> |2:Select query on Replica DB| id4["Database Server"]
}}


=== 방법 1: 자체적으로 바닥에서부터 개발 ===
=== 방법 1: 자체적으로 바닥에서부터 개발 ===
43번째 줄: 60번째 줄:
</syntaxhighlight>
</syntaxhighlight>


=== 방법 2: [[mw:Extension:RatePage|RatePage 확장 기능]]에 의존(권장) ===
=== 방법 2: [[mw:Extension:VoteNY|<s>RatePage</s>VoteNY 확장 기능]]에 의존(권장) ===
[[mw:Extension:RatePage|리버티책에서 사용 중인 RatePage 확장 기능]]을 사용하는 방법이 존재: MultimediaViewer -> RatePage -> SectionRatings(가칭) 순으로 의존성 발생
[[mw:Extension:VoteNY|VoteNY 확장 기능]]을 사용하는 방법이 존재: VoteNY -> SectionRatings(가칭) 순으로 의존성 발생
* extension.json의 "requires"에 <nowiki>["extensions"]["RatePage"]</nowiki>의 값을 추가할 것
* 기존에 고려하던 [[mw:Extension:RatePage|RatePage]]는 1.37+ 버전 미디어위키에 제대로 대응하지 못하는 것을 확인: Contest 관련 기능에 오류가 발생하여 원작자에게 제보한 결과, 확장 기능 개발을 현재 주력으로 하고 있지 않아 해결이 느릴 수 있다고 답변하였기에, 대체품으로 고려하던 VoteNY으로 전환
* extension.json의 "requires"에 <nowiki>["extensions"]["VoteNY"]</nowiki>의 값을 추가할 것 - {{완료}}
* 다만 이럴 경우 기본적으로 기존 평가를 반영하지 못함
* 다만 이럴 경우 기본적으로 기존 평가를 반영하지 못함
** 이 RatePage에 기존 평가 문단을 파싱하여 ratepage_vote 데이터베이스 테이블 편집으로 점수를 초기화하는 업데이트 스크립트 파일(단일 php 파일)를 추가하는 방법도 가능(SectionRatings 확장 기능의 maintenance 폴더를 만들어 거기 넣을 것)
** 데이터베이스에에 기존 평가 문단을 파싱하여 ratepage_vote 데이터베이스 테이블 편집으로 점수를 초기화하는 업데이트 스크립트 파일(단일 php 파일)를 추가하는 방법도 가능(SectionRatings 확장 기능의 maintenance 폴더를 만들어 거기 넣을 것)
** 기존 평가는 자세한 평론을 위해 위해 그대로 두고, SectionRatings는 RatePage가 만드는 데이터베이스 테이블인 ratepage_contest -> (특정한 "rpc_id")의 값을 사용해 ratepage_vote의 행을 선택하고 정렬하여 상위 k개의 값을 배열로 반환하여 특정 미디어위키 태그에 붙이는 작업을 수행
** 기존 평가는 자세한 평론을 위해 위해 그대로 두고, SectionRatings는 VoteNY가 만드는 데이터베이스 테이블인 Vote에서 다음 형식을 참고해 행을 선택하고 정렬하여 상위 k개의 값을 배열로 반환하여 특정 미디어위키 태그에 붙이는 작업을 수행(아래 행은 VoteNY의 vote.mysql 문서에 있는 Vote 테이블 생성 구문임, GPL 2.0 or later로 배포됨)
** 해결해야 하는 문제: 유저와 IP를 동시에 ratepage_vote 데이터베이스 테이블에 기록해야 하는데, 이전 유저의 투표를 파싱해 수동 추가할 때 IP는 0.0.0.0으로 해도 먹힐 것 같지만, 익명 유저의 투표를 어떻게 웹 페이지에서 파싱해 걸러낼 것인가?
*** SQL 파일 push로 수행하며, auto_increment 적용되는 vote_id column을 제외하고, vote_actor는 음수로 설정하며, vote_page_id와 vote_value만 반드시 설정할 것
*** 그냥 'anonUserN'(마지막 N은 임의의 숫자)로 계정 이름을 정할 수 있긴 함
<syntaxhighlight lang='sql'>
*** 정규표현식 사용 대량 편집 시 다음 구문을 찾아야 IP 주소 투표를 찾을 수 있지만, 아예 서명을 안 한 평가도 존재하여 정규 표현식 파싱은 사용하는 것을 권장하지 않음, 그냥 번호를 포함하는 1단계 들여쓰기 된 평가 갯수만 문단 별로 구하면 될 것임.
CREATE TABLE IF NOT EXISTS /*_*/Vote (
**** <pre>/#([\s0-9A-Za-zㄱ-ㅎ가-핳])+([0-9]){1,3}\.([0-9]){1,3}\.([0-9]){1,3}\.([0-9]){1,3}/</pre>
  -- Internal ID to identify between different vote tags on different pages
*** 아니면 따로 평가 문단만 게임 토론 페이지 별로 action=raw 쿼리를 보내 긁어와 텍스트 파일로 받는 자바스크립트 코드를 만들 수도 있음(테스트 서버에서 먼저 시행)
  vote_id int(11) NOT NULL PRIMARY KEY auto_increment,
  -- Key to actor_id for the person who voted
  vote_actor bigint unsigned NOT NULL,
  -- Username (if any) of the person who voted
  username varchar(255) NOT NULL default 0,
  -- User ID of the person who voted
  vote_user_id int(11) NOT NULL default 0,
  -- ID of the page where the vote tag is in
  vote_page_id int(11) NOT NULL default 0,
  -- Value of the vote (ranging from 1 to 5)
  vote_value char(1) character set latin1 collate latin1_bin NOT NULL default '',
  -- Timestamp when the vote was cast
  vote_date datetime NOT NULL default '1970-01-01 00:00:01',
  -- IP address of the user who voted
  vote_ip varchar(45) NOT NULL default ''
)
</syntaxhighlight>
* 참고: RatePage의 테이블 구조는 다음과 같음
<syntaxhighlight lang='sql'>
CREATE TABLE IF NOT EXISTS /*_*/ratepage_vote (
  `rv_page_id` int(10) unsigned NOT NULL,
  `rv_user` varbinary(255) NOT NULL,
  `rv_ip` varbinary(255) default NULL,
  `rv_answer` int(3) NOT NULL,
  `rv_date` datetime NOT NULL,
  `rv_contest` varbinary(255) NOT NULL DEFAULT '',
  PRIMARY KEY  (`rv_page_id`, `rv_contest`, `rv_user`)
)
</syntaxhighlight>
* 해결해야 하는 문제: 유저와 IP를 동시에 Vote 데이터베이스 테이블에 기록해야 하는데, 이전 유저의 투표를 파싱해 수동 추가할 때 IP는 0.0.0.0으로 해도 먹힐 것 같지만, 익명 유저의 ID를 어떻게 웹 페이지에서 파싱해 걸러낼 것인가?
** <s>그냥 'anonUserN'(마지막 N은 임의의 숫자)로 계정 이름을 정할 수 있긴 함</s> VoteNY에서는 ID 기본값이 지정되어 있어 상관 없음
** 정규표현식 사용 대량 편집 시 다음 구문을 찾아야 IP 주소 투표를 찾을 수 있지만, 아예 서명을 안 한 평가도 존재하여 정규 표현식 파싱은 사용하는 것을 권장하지 않음, 그냥 번호를 포함하는 1단계 들여쓰기 된 평가 갯수만 문단 별로 구하면 될 것임.
*** <pre>/#([\s0-9A-Za-zㄱ-ㅎ가-핳])+([0-9]){1,3}\.([0-9]){1,3}\.([0-9]){1,3}\.([0-9]){1,3}/</pre>
** 아니면 따로 평가 문단만 게임 토론 페이지 별로 action=raw 쿼리를 보내 긁어와 텍스트 파일로 받는 자바스크립트 코드를 만들 수도 있음(테스트 서버에서 먼저 시행)
 
== 추가 기능: 장르별 분류 ==
[[mw:Manual:Categorylinks_table]]을 참고하여 토론 문서에도 game.json에 따른 분류 추가 후 다음 쿼리를 응용하여 장르별 인기 게임을 가져옴
<syntaxhighlight lang='sql'>
SELECT vote_page_id, COUNT(vote_value) AS vote_count, AVG(vote_value) AS vote_avg FROM categorylinks INNER JOIN Vote ON categorylinks.cl_from Vote.vote_page_id WHERE categorylinks.cl_to = "(이름 공간 제외하고 쿼리한 분류 이름)" GROUP BY vote_page_id;
</syntaxhighlight>


== 참고 자료 ==
== 참고 자료 ==
60번째 줄: 117번째 줄:
* [[mw:API:Calling internally|Mediawiki API를 서버 내에서 호출하는 방법]]
* [[mw:API:Calling internally|Mediawiki API를 서버 내에서 호출하는 방법]]
** [[mw:API:Main page|미디어위키 API 목록]]
** [[mw:API:Main page|미디어위키 API 목록]]
* [[mw:Extension:RatePage|리버티책에서 사용 중인 RatePage 확장 기능]]
** [https://www.php.net/manual/en/book.curl.php 내부 호출용 PHP cURL 관련 함수]
** [[mw:Extension:VoteNY|유사하게 페이지 평가가 가능한 VoteNY 확장 기능]]
* [https://doc.wikimedia.org/mediawiki-core/master/php/index.html 미디어위키 코어 PHP]
* [[mw:API:REST_API|미디어위키 REST API]]
** [[mw:API:REST_API/Extensions|REST API 사용 확장기능 만들기]]
* [[mw:Extension:VoteNY|VoteNY 확장 기능]]
** [[mw:Extension:RatePage|리버티책에서 사용 중인 RatePage 확장 기능]]

2024년 9월 17일 (화) 22:28 기준 최신판

게임별 토론 문서의 게임 평가용 문단 내용을 바탕으로 SQL 쿼리를 사용해 이 게임의 평점이 리버티게임 전체에서 상위 몇 퍼센트인지 알려주는 SectionRatings(가칭)의 설계에 관한 문서입니다.

  • TODO: UML의 문법에 따라 클래스 다이어그램, 상태 다이어그램(FSM도 좋지만 가능하면 순서도로), 데이터 흐름 명시할 것

요구 사항

== h-게임 평가하기-(id) ==
h-(사용자1)-(id)
=== h-(문단)-게임 평가하기-(id) ===
c-(사용자2)-(id)-게임 평가하기
c-(사용자2)-(id)
:c-(사용자3)-(id)-(사용자2)-(id)
:c-(사용자3)-(id)
  • 현재 틀:평론은 백괴게임 시절과 문단 제목이 다른 부분(이 게임 평가하기, 평가, 게임 평가하기 등등)도 있고, 토론 문서가 없는 게임들이 아직 있어 요구 사항처럼 대량 편집을 넣기 쉽지 않은 부분이 있음
    • 다만 5단계의 하위 평가 문단은 문단 제목 문구가 전부 같은 것으로 추정되므로, 이에 따라 키워드를 뽑아 파싱하는 것은 가능 -> action=raw&section=1로 API 호출 or REST API 사용
      • REST API로 대문 정보 가져오기: 이곳의 대문 가져오는 예시 -> "source" 키에 페이지 내용 있음
      • '== 평가 =='나 '== 이 게임 평가하기 =='를 '== 게임 평가하기 =='로 먼저 대량 편집으로 통일
    • 코멘트 편집의 경우 서명을 안 넣은 평가들이 가끔 발견되어, 정확히 파싱하기 어려움, 평론 틀을 사용하지 않고 토론 양식을 만든 게임도 고려해야 함(RPG in City, 투표 게임의 사례)
  • 클라이언트 측에서 토론 문서를 보았을 때 평론 틀이 풀기 처리된 토론 문서의 HTML element 계층 구조는 같은 level을 가지고 있어 클라이언트 측 파싱은 매우 복잡하거나 불가능할 가능성이 큼(h2, h3, ol 태그가 전부 같은 부모를 가짐)
  • 자체 개발이 좋으나 PHP 확장 기능 개발의 어려움으로 인해 실패할 가능성이 크므로 두 가지 방법을 고려새삼 조사 확장 기능을 개발한 *devunt님이 대단해지는 순간

백엔드 개발 공통 명세

  • mw:API:REST API/Extensions의 내용에 따라 REST API를 작성하여 다음과 같은 URL로 평가 요청(첫 평가는 PUT, 변경 시 POST)하도록 할 것: (예정)
    • 예: dev.libertygame.work/rest.php/sectionratings/v0/rategame/(게임 이름:문자열)/(별점:정수)
    • 반환 결과인 JSON 객체는 "Success"/"Fail" 같은 값으로 표시한 평가 성공 여부를 포함해야 함
  • 평가 취합은 GET 요청으로 다음 REST API 작성: 완료

프론트엔드 개발 명세 Authentication 문제로 인해 SQL 파일 작성으로 대체됨

  • 도구 스크립트 - 커먼자스로 아래 내용에 해당하는 코드를 옮길 것
  • 최대 4개를 가져오도록 요청한 다음 백엔드에서 가져온 데이터를 바탕으로 틀:게임카드에 적힌 대로 ','(쉼표)로 구분한 게임카드 파라미터를 구성
    • 그 다음 action=parse 미디어위키 API로 미디어위키:Gadget-gamelistjs.js와 유사하게[1] 게임카드 틀에 대한 렌더링을 요청하는 Promise를 작성하여 스크립트로 추가

방법 1: 자체적으로 바닥에서부터 개발

  • 자체적으로 SQL 파일을 만들어 libertygame.sectionratings 데이터베이스 테이블을 생성
  • 서버에서 php update.php를 돌리면 SQL 파일의 내용대로 쿼리 전송
  • 문제점1: 중복 투표를 어떻게 걸러낼 것인가? -> 미리 결과값을 보내고, 서버 단에서 쿼리로 이미 값이 있는지
  • 문제점2: Widget을 어떻게 설계할 것인가?
  • 다음 sectionratings 테이블을 libertygame 데이터베이스에 추가: primary 키 지정 필요 없음(모든 게임이 공통적으로 이 테이블을 사용 예정이라 값이 고유할 필요 없음)
CREATE TABLE IF NOT EXISTS sectionratings(
    'sr_date' date NOT NULL,
    'sr_gamename' varchar(63) NOT NULL,
    'sr_user' varbinary(255) NOT NULL,
    'sr_ip' varbinary(255) DEFAULT NULL,
    'sr_rate' int NOT NULL
);

방법 2: RatePageVoteNY 확장 기능에 의존(권장)

VoteNY 확장 기능을 사용하는 방법이 존재: VoteNY -> SectionRatings(가칭) 순으로 의존성 발생

  • 기존에 고려하던 RatePage는 1.37+ 버전 미디어위키에 제대로 대응하지 못하는 것을 확인: Contest 관련 기능에 오류가 발생하여 원작자에게 제보한 결과, 확장 기능 개발을 현재 주력으로 하고 있지 않아 해결이 느릴 수 있다고 답변하였기에, 대체품으로 고려하던 VoteNY으로 전환
  • extension.json의 "requires"에 ["extensions"]["VoteNY"]의 값을 추가할 것 - 완료
  • 다만 이럴 경우 기본적으로 기존 평가를 반영하지 못함
    • 데이터베이스에에 기존 평가 문단을 파싱하여 ratepage_vote 데이터베이스 테이블 편집으로 점수를 초기화하는 업데이트 스크립트 파일(단일 php 파일)를 추가하는 방법도 가능(SectionRatings 확장 기능의 maintenance 폴더를 만들어 거기 넣을 것)
    • 기존 평가는 자세한 평론을 위해 위해 그대로 두고, SectionRatings는 VoteNY가 만드는 데이터베이스 테이블인 Vote에서 다음 형식을 참고해 행을 선택하고 정렬하여 상위 k개의 값을 배열로 반환하여 특정 미디어위키 태그에 붙이는 작업을 수행(아래 행은 VoteNY의 vote.mysql 문서에 있는 Vote 테이블 생성 구문임, GPL 2.0 or later로 배포됨)
      • SQL 파일 push로 수행하며, auto_increment 적용되는 vote_id column을 제외하고, vote_actor는 음수로 설정하며, vote_page_id와 vote_value만 반드시 설정할 것
CREATE TABLE IF NOT EXISTS /*_*/Vote (
  -- Internal ID to identify between different vote tags on different pages
  vote_id int(11) NOT NULL PRIMARY KEY auto_increment,
  -- Key to actor_id for the person who voted
  vote_actor bigint unsigned NOT NULL,
  -- Username (if any) of the person who voted
  username varchar(255) NOT NULL default 0,
  -- User ID of the person who voted
  vote_user_id int(11) NOT NULL default 0,
  -- ID of the page where the vote tag is in
  vote_page_id int(11) NOT NULL default 0,
  -- Value of the vote (ranging from 1 to 5)
  vote_value char(1) character set latin1 collate latin1_bin NOT NULL default '',
  -- Timestamp when the vote was cast
  vote_date datetime NOT NULL default '1970-01-01 00:00:01',
  -- IP address of the user who voted
  vote_ip varchar(45) NOT NULL default ''
)
  • 참고: RatePage의 테이블 구조는 다음과 같음
CREATE TABLE IF NOT EXISTS /*_*/ratepage_vote (
  `rv_page_id` int(10) unsigned NOT NULL,
  `rv_user` varbinary(255) NOT NULL,
  `rv_ip` varbinary(255) default NULL,
  `rv_answer` int(3) NOT NULL,
  `rv_date` datetime NOT NULL,
  `rv_contest` varbinary(255) NOT NULL DEFAULT '',
  PRIMARY KEY  (`rv_page_id`, `rv_contest`, `rv_user`)
)
  • 해결해야 하는 문제: 유저와 IP를 동시에 Vote 데이터베이스 테이블에 기록해야 하는데, 이전 유저의 투표를 파싱해 수동 추가할 때 IP는 0.0.0.0으로 해도 먹힐 것 같지만, 익명 유저의 ID를 어떻게 웹 페이지에서 파싱해 걸러낼 것인가?
    • 그냥 'anonUserN'(마지막 N은 임의의 숫자)로 계정 이름을 정할 수 있긴 함 VoteNY에서는 ID 기본값이 지정되어 있어 상관 없음
    • 정규표현식 사용 대량 편집 시 다음 구문을 찾아야 IP 주소 투표를 찾을 수 있지만, 아예 서명을 안 한 평가도 존재하여 정규 표현식 파싱은 사용하는 것을 권장하지 않음, 그냥 번호를 포함하는 1단계 들여쓰기 된 평가 갯수만 문단 별로 구하면 될 것임.
      • /#([\s0-9A-Za-zㄱ-ㅎ가-핳])+([0-9]){1,3}\.([0-9]){1,3}\.([0-9]){1,3}\.([0-9]){1,3}/
    • 아니면 따로 평가 문단만 게임 토론 페이지 별로 action=raw 쿼리를 보내 긁어와 텍스트 파일로 받는 자바스크립트 코드를 만들 수도 있음(테스트 서버에서 먼저 시행)

추가 기능: 장르별 분류

mw:Manual:Categorylinks_table을 참고하여 토론 문서에도 game.json에 따른 분류 추가 후 다음 쿼리를 응용하여 장르별 인기 게임을 가져옴

SELECT vote_page_id, COUNT(vote_value) AS vote_count, AVG(vote_value) AS vote_avg FROM categorylinks INNER JOIN Vote ON categorylinks.cl_from Vote.vote_page_id WHERE categorylinks.cl_to = "(이름 공간 제외하고 쿼리한 분류 이름)" GROUP BY vote_page_id;

참고 자료

  1. 단, 문서 제목을 넘기지 않고 내용을 직접 파라미터로 렌더링해야 함