Function bodies 254 total
main function · go · L18-L49 (32 LOC)backend/cmd/monkey-planner/main.go
func main() {
if len(os.Args) > 1 && os.Args[1] == "mcp" {
runMCP()
return
}
addr := getenv("MP_ADDR", ":8080")
dsn := getenv("MP_DSN", defaultDSN())
repo, err := storage.NewRepo(dsn)
if err != nil {
log.Fatalf("storage open: %v", err)
}
defer repo.Close()
svc := service.New(repo, nil)
var static fs.FS
if dist, err := web.Dist(); err == nil {
static = dist
log.Printf("monkey-planner: prod build — embedded frontend enabled")
} else {
log.Printf("monkey-planner: dev build — run Vite dev server at :5173 for UI (%v)", err)
}
router := mphttp.NewRouter(svc, static)
log.Printf("monkey-planner listening on %s (dsn=%s)", addr, dsn)
if err := http.ListenAndServe(addr, router); err != nil {
log.Fatalf("server error: %v", err)
}
}getenv function · go · L56-L61 (6 LOC)backend/cmd/monkey-planner/main.go
func getenv(k, d string) string {
if v := os.Getenv(k); v != "" {
return v
}
return d
}runMCP function · go · L18-L56 (39 LOC)backend/cmd/monkey-planner/mcp.go
func runMCP() {
// All logging to stderr; stdout is reserved for JSON-RPC protocol.
log.SetOutput(os.Stderr)
dsn := getenv("MP_DSN", defaultDSN())
repo, err := storage.NewRepo(dsn)
if err != nil {
log.Fatalf("mcp: storage open: %v", err)
}
defer repo.Close()
svc := service.New(repo, nil)
ctx := context.Background()
scanner := bufio.NewScanner(os.Stdin)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024) // 1MB buffer
for scanner.Scan() {
line := scanner.Bytes()
if len(line) == 0 {
continue
}
var req jsonRPCRequest
if err := json.Unmarshal(line, &req); err != nil {
continue
}
resp := handleMCPRequest(ctx, svc, &req)
if resp == nil {
// Notification: no response needed.
continue
}
out, _ := json.Marshal(resp)
fmt.Fprintln(os.Stdout, string(out))
}
}handleMCPRequest function · go · L76-L109 (34 LOC)backend/cmd/monkey-planner/mcp.go
func handleMCPRequest(ctx context.Context, svc *service.Service, req *jsonRPCRequest) *jsonRPCResponse {
switch req.Method {
case "initialize":
return &jsonRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: map[string]any{
"protocolVersion": "2024-11-05",
"capabilities": map[string]any{"tools": map[string]any{}},
"serverInfo": map[string]any{"name": "monkey-planner", "version": "1.0.0"},
},
}
case "notifications/initialized":
return nil // notification, no response
case "tools/list":
return &jsonRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: map[string]any{"tools": mcpToolDefinitions()},
}
case "tools/call":
return handleMCPToolCall(ctx, svc, req)
default:
return &jsonRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Error: map[string]any{"code": -32601, "message": "method not found"},
}
}
}mcpToolDefinitions function · go · L113-L254 (142 LOC)backend/cmd/monkey-planner/mcp.go
func mcpToolDefinitions() []map[string]any {
return []map[string]any{
{
"name": "list_boards",
"description": "List all boards",
"inputSchema": map[string]any{
"type": "object",
"properties": map[string]any{},
},
},
{
"name": "list_issues",
"description": "List issues. Filter by boardId and/or status (Pending, Approved, InProgress, QA, Done, Rejected)",
"inputSchema": map[string]any{
"type": "object",
"properties": map[string]any{
"boardId": map[string]any{"type": "string", "description": "Filter by board ID"},
"status": map[string]any{"type": "string", "description": "Filter by status: Pending, Approved, InProgress, Done, Rejected"},
},
},
},
{
"name": "get_issue",
"description": "Get full issue detail including instructions, criteria, and comments",
"inputSchema": map[string]any{
"type": "object",
"properties": map[string]any{
"issueId": map[string]any{"type": "string", handleMCPToolCall function · go · L258-L291 (34 LOC)backend/cmd/monkey-planner/mcp.go
func handleMCPToolCall(ctx context.Context, svc *service.Service, req *jsonRPCRequest) *jsonRPCResponse {
var params struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
return &jsonRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Error: map[string]any{"code": -32602, "message": "invalid params"},
}
}
result, err := mcpCallTool(ctx, svc, params.Name, params.Arguments)
if err != nil {
return &jsonRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: map[string]any{
"content": []map[string]any{{"type": "text", "text": "Error: " + err.Error()}},
"isError": true,
},
}
}
jsonBytes, _ := json.MarshalIndent(result, "", " ")
return &jsonRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
Result: map[string]any{
"content": []map[string]any{{"type": "text", "text": string(jsonBytes)}},
},
}
}mcpCallTool function · go · L293-L503 (211 LOC)backend/cmd/monkey-planner/mcp.go
func mcpCallTool(ctx context.Context, svc *service.Service, name string, argsRaw json.RawMessage) (any, error) {
switch name {
case "list_boards":
return svc.ListBoards(ctx)
case "list_issues":
var args struct {
BoardID string `json:"boardId"`
Status string `json:"status"`
}
_ = json.Unmarshal(argsRaw, &args)
var f storage.IssueFilter
if args.BoardID != "" {
f.BoardID = &args.BoardID
}
if args.Status != "" {
st := domain.Status(args.Status)
f.Status = &st
}
return svc.ListIssues(ctx, f)
case "get_issue":
var args struct {
IssueID string `json:"issueId"`
}
if err := json.Unmarshal(argsRaw, &args); err != nil || args.IssueID == "" {
return nil, fmt.Errorf("issueId is required")
}
issue, children, err := svc.GetIssue(ctx, args.IssueID)
if err != nil {
return nil, err
}
comments, _ := svc.ListComments(ctx, args.IssueID)
return map[string]any{
"issue": issue,
"children": children,
"comments": comments,
}, niCitation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
Valid method · go · L13-L15 (3 LOC)backend/internal/domain/board.go
func (v ViewType) Valid() bool {
return v == ViewKanban || v == ViewList
}Valid method · go · L21-L27 (7 LOC)backend/internal/domain/issue.go
func (s Status) Valid() bool {
switch s {
case StatusPending, StatusApproved, StatusInProgress, StatusQA, StatusDone, StatusRejected:
return true
}
return false
}ValidateTransition function · go · L72-L113 (42 LOC)backend/internal/domain/issue.go
func ValidateTransition(from, to Status) error {
if !from.Valid() || !to.Valid() {
return ErrInvalidStatus
}
if from == to {
return ErrSelfSameTransition
}
// Rejected는 터미널 상태
if from == StatusRejected {
return ErrUnknownTransition
}
// Pending에서는 Approve 버튼 또는 Reject만 사용 가능
if from == StatusPending {
if to == StatusApproved {
return ErrDirectApproval
}
if to == StatusRejected {
return nil
}
return ErrUnknownTransition
}
// Pending/Rejected로 되돌리기 불가
if to == StatusPending || to == StatusRejected {
return ErrUnknownTransition
}
if to == StatusApproved {
return ErrDirectApproval
}
// 허용되는 전이 정의
allowed := map[Status][]Status{
StatusApproved: {StatusInProgress},
StatusInProgress: {StatusQA},
StatusQA: {StatusDone, StatusInProgress},
StatusDone: {StatusQA},
}
for _, s := range allowed[from] {
if s == to {
return nil
}
}
return ErrUnknownTransition
}Valid method · go · L17-L23 (7 LOC)backend/internal/domain/property.go
func (p PropertyType) Valid() bool {
switch p {
case PropText, PropNumber, PropSelect, PropMultiSelect, PropDate, PropCheckbox:
return true
}
return false
}Valid method · go · L22-L29 (8 LOC)backend/internal/domain/webhook.go
func (e WebhookEvent) Valid() bool {
for _, v := range AllWebhookEvents {
if v == e {
return true
}
}
return false
}list method · go · L15-L25 (11 LOC)backend/internal/http/board_handler.go
func (h *boardHandler) list(w http.ResponseWriter, r *http.Request) {
out, err := h.svc.ListBoards(r.Context())
if err != nil {
mapError(w, err)
return
}
if out == nil {
out = []domain.Board{}
}
writeJSON(w, http.StatusOK, out)
}create method · go · L27-L42 (16 LOC)backend/internal/http/board_handler.go
func (h *boardHandler) create(w http.ResponseWriter, r *http.Request) {
var in struct {
Name string `json:"name"`
ViewType domain.ViewType `json:"viewType"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
out, err := h.svc.CreateBoard(r.Context(), in.Name, in.ViewType)
if err != nil {
mapError(w, err)
return
}
writeJSON(w, http.StatusCreated, out)
}patch method · go · L44-L60 (17 LOC)backend/internal/http/board_handler.go
func (h *boardHandler) patch(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
var in struct {
Name *string `json:"name,omitempty"`
ViewType *domain.ViewType `json:"viewType,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
out, err := h.svc.UpdateBoard(r.Context(), id, in.Name, in.ViewType)
if err != nil {
mapError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}All rows above produced by Repobility · https://repobility.com
delete method · go · L62-L69 (8 LOC)backend/internal/http/board_handler.go
func (h *boardHandler) delete(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.svc.DeleteBoard(r.Context(), id); err != nil {
mapError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}month method · go · L16-L36 (21 LOC)backend/internal/http/calendar_handler.go
func (h *calendarHandler) month(w http.ResponseWriter, r *http.Request) {
year, err := strconv.Atoi(r.URL.Query().Get("year"))
if err != nil || year < 1970 || year > 9999 {
writeErr(w, http.StatusBadRequest, "invalid_year", "year must be 1970–9999")
return
}
monthNum, err := strconv.Atoi(r.URL.Query().Get("month"))
if err != nil || monthNum < 1 || monthNum > 12 {
writeErr(w, http.StatusBadRequest, "invalid_month", "month must be 1–12")
return
}
out, err := h.svc.GetMonthStats(r.Context(), year, time.Month(monthNum))
if err != nil {
mapError(w, err)
return
}
if out == nil {
out = []storage.DayCount{}
}
writeJSON(w, http.StatusOK, out)
}day method · go · L39-L62 (24 LOC)backend/internal/http/calendar_handler.go
func (h *calendarHandler) day(w http.ResponseWriter, r *http.Request) {
dateStr := r.URL.Query().Get("date")
day, err := time.Parse("2006-01-02", dateStr)
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid_date", "date must be YYYY-MM-DD")
return
}
out, err := h.svc.GetDayStats(r.Context(), day.UTC())
if err != nil {
mapError(w, err)
return
}
// nil slice → [] 로 정규화 (클라이언트가 .length 접근 시 TypeError 방지)
if out.Created == nil {
out.Created = []domain.Issue{}
}
if out.Approved == nil {
out.Approved = []domain.Issue{}
}
if out.Completed == nil {
out.Completed = []domain.Issue{}
}
writeJSON(w, http.StatusOK, out)
}list method · go · L15-L26 (12 LOC)backend/internal/http/comment_handler.go
func (h *commentHandler) list(w http.ResponseWriter, r *http.Request) {
issueID := chi.URLParam(r, "issueId")
comments, err := h.svc.ListComments(r.Context(), issueID)
if err != nil {
mapError(w, err)
return
}
if comments == nil {
comments = []domain.Comment{}
}
writeJSON(w, http.StatusOK, comments)
}create method · go · L28-L43 (16 LOC)backend/internal/http/comment_handler.go
func (h *commentHandler) create(w http.ResponseWriter, r *http.Request) {
issueID := chi.URLParam(r, "issueId")
var in struct {
Body string `json:"body"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
c, err := h.svc.CreateComment(r.Context(), issueID, in.Body)
if err != nil {
mapError(w, err)
return
}
writeJSON(w, http.StatusCreated, c)
}delete method · go · L45-L52 (8 LOC)backend/internal/http/comment_handler.go
func (h *commentHandler) delete(w http.ResponseWriter, r *http.Request) {
commentID := chi.URLParam(r, "commentId")
if err := h.svc.DeleteComment(r.Context(), commentID); err != nil {
mapError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}writeJSON function · go · L21-L30 (10 LOC)backend/internal/http/errors.go
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
if body == nil {
return
}
if err := json.NewEncoder(w).Encode(body); err != nil {
log.Printf("writeJSON: %v", err)
}
}writeErr function · go · L32-L37 (6 LOC)backend/internal/http/errors.go
func writeErr(w http.ResponseWriter, status int, code, msg string) {
b := errBody{}
b.Error.Code = code
b.Error.Message = msg
writeJSON(w, status, b)
}Repobility — same analyzer, your code, free for public repos · /scan/
mapError function · go · L40-L61 (22 LOC)backend/internal/http/errors.go
func mapError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, storage.ErrNotFound):
writeErr(w, http.StatusNotFound, "not_found", err.Error())
case errors.Is(err, storage.ErrCycle):
writeErr(w, http.StatusBadRequest, "cycle", "parent_id would create a cycle")
case errors.Is(err, storage.ErrConflict):
writeErr(w, http.StatusConflict, "conflict", err.Error())
case errors.Is(err, service.ErrApproveViaPatch):
writeErr(w, http.StatusConflict, "use_approve_endpoint",
"use POST /api/issues/:id/approve")
case errors.Is(err, service.ErrBackwardTransition):
writeErr(w, http.StatusBadRequest, "backward_transition", err.Error())
case errors.Is(err, service.ErrInvalidTransition),
errors.Is(err, service.ErrEmptyTitle),
errors.Is(err, service.ErrMissingBoard):
writeErr(w, http.StatusBadRequest, "invalid_input", err.Error())
default:
log.Printf("http: unhandled err: %v", err)
writeErr(w, http.StatusInternalServerError, "internal", "internal server error"list method · go · L16-L43 (28 LOC)backend/internal/http/issue_handler.go
func (h *issueHandler) list(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
var f storage.IssueFilter
if v := q.Get("board_id"); v != "" {
f.BoardID = &v
}
if v := q.Get("status"); v != "" {
s := domain.Status(v)
if !s.Valid() {
writeErr(w, http.StatusBadRequest, "invalid_status", "unknown status")
return
}
f.Status = &s
}
if q.Has("parent_id") {
v := q.Get("parent_id") // 빈 문자열이면 루트 필터
f.ParentID = &v
}
out, err := h.svc.ListIssues(r.Context(), f)
if err != nil {
mapError(w, err)
return
}
if out == nil {
out = []domain.Issue{}
}
writeJSON(w, http.StatusOK, out)
}get method · go · L45-L59 (15 LOC)backend/internal/http/issue_handler.go
func (h *issueHandler) get(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
iss, children, err := h.svc.GetIssue(r.Context(), id)
if err != nil {
mapError(w, err)
return
}
if children == nil {
children = []domain.Issue{}
}
writeJSON(w, http.StatusOK, map[string]any{
"issue": iss,
"children": children,
})
}create method · go · L61-L83 (23 LOC)backend/internal/http/issue_handler.go
func (h *issueHandler) create(w http.ResponseWriter, r *http.Request) {
var in struct {
BoardID string `json:"boardId"`
ParentID *string `json:"parentId,omitempty"`
Title string `json:"title"`
Body string `json:"body"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
out, err := h.svc.CreateIssue(r.Context(), service.CreateIssueInput{
BoardID: in.BoardID,
ParentID: in.ParentID,
Title: in.Title,
Body: in.Body,
})
if err != nil {
mapError(w, err)
return
}
writeJSON(w, http.StatusCreated, out)
}patch method · go · L85-L173 (89 LOC)backend/internal/http/issue_handler.go
func (h *issueHandler) patch(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
// parentId 는 3상태: 없음(미변경) / null(루트) / 문자열(지정)
raw := map[string]json.RawMessage{}
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
var in service.UpdateIssueInput
if v, ok := raw["title"]; ok {
var s string
if err := json.Unmarshal(v, &s); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_title", err.Error())
return
}
in.Title = &s
}
if v, ok := raw["body"]; ok {
var s string
if err := json.Unmarshal(v, &s); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_body", err.Error())
return
}
in.Body = &s
}
if v, ok := raw["instructions"]; ok {
var s string
if err := json.Unmarshal(v, &s); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_instructions", err.Error())
return
}
in.Instructions = &s
}
if v, ok := raw["status"]; ok {
approve method · go · L175-L183 (9 LOC)backend/internal/http/issue_handler.go
func (h *issueHandler) approve(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
out, err := h.svc.ApproveIssue(r.Context(), id)
if err != nil {
mapError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}delete method · go · L185-L192 (8 LOC)backend/internal/http/issue_handler.go
func (h *issueHandler) delete(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.svc.DeleteIssue(r.Context(), id); err != nil {
mapError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}addDependency method · go · L194-L212 (19 LOC)backend/internal/http/issue_handler.go
func (h *issueHandler) addDependency(w http.ResponseWriter, r *http.Request) {
issueID := chi.URLParam(r, "issueId")
var in struct {
BlockerID string `json:"blockerId"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
if in.BlockerID == "" {
writeErr(w, http.StatusBadRequest, "missing_blocker_id", "blockerId is required")
return
}
if err := h.svc.AddDependency(r.Context(), in.BlockerID, issueID); err != nil {
mapError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}Repobility · open methodology · https://repobility.com/research/
removeDependency method · go · L214-L222 (9 LOC)backend/internal/http/issue_handler.go
func (h *issueHandler) removeDependency(w http.ResponseWriter, r *http.Request) {
issueID := chi.URLParam(r, "issueId")
blockerID := chi.URLParam(r, "blockerId")
if err := h.svc.RemoveDependency(r.Context(), blockerID, issueID); err != nil {
mapError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}reorder method · go · L224-L237 (14 LOC)backend/internal/http/issue_handler.go
func (h *issueHandler) reorder(w http.ResponseWriter, r *http.Request) {
var in struct {
IssueIDs []string `json:"issueIds"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
if err := h.svc.ReorderIssues(r.Context(), in.IssueIDs); err != nil {
mapError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}ValidateUTF8 function · go · L13-L41 (29 LOC)backend/internal/http/middleware.go
func ValidateUTF8(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Body == nil || r.ContentLength == 0 {
next.ServeHTTP(w, r)
return
}
ct := r.Header.Get("Content-Type")
if ct == "" || !isJSON(ct) {
next.ServeHTTP(w, r)
return
}
body, err := io.ReadAll(r.Body)
r.Body.Close()
if err != nil {
writeErr(w, http.StatusBadRequest, "read_error", "failed to read request body")
return
}
if !utf8.Valid(body) {
writeErr(w, http.StatusBadRequest, "invalid_encoding",
"request body contains non-UTF-8 bytes; ensure your client sends UTF-8 encoded JSON")
return
}
r.Body = io.NopCloser(bytes.NewReader(body))
next.ServeHTTP(w, r)
})
}isJSON function · go · L43-L46 (4 LOC)backend/internal/http/middleware.go
func isJSON(ct string) bool {
return len(ct) >= 16 && ct[:16] == "application/json" ||
len(ct) >= 4 && ct[:4] == "text"
}list method · go · L15-L26 (12 LOC)backend/internal/http/property_handler.go
func (h *propertyHandler) list(w http.ResponseWriter, r *http.Request) {
boardID := chi.URLParam(r, "boardId")
out, err := h.svc.ListBoardProperties(r.Context(), boardID)
if err != nil {
mapError(w, err)
return
}
if out == nil {
out = []domain.BoardProperty{}
}
writeJSON(w, http.StatusOK, out)
}create method · go · L28-L45 (18 LOC)backend/internal/http/property_handler.go
func (h *propertyHandler) create(w http.ResponseWriter, r *http.Request) {
boardID := chi.URLParam(r, "boardId")
var in struct {
Name string `json:"name"`
Type domain.PropertyType `json:"type"`
Options []string `json:"options"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
out, err := h.svc.CreateBoardProperty(r.Context(), boardID, in.Name, in.Type, in.Options)
if err != nil {
mapError(w, err)
return
}
writeJSON(w, http.StatusCreated, out)
}update method · go · L47-L64 (18 LOC)backend/internal/http/property_handler.go
func (h *propertyHandler) update(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "propId")
var in struct {
Name *string `json:"name,omitempty"`
Options *[]string `json:"options,omitempty"`
Position *int `json:"position,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
out, err := h.svc.UpdateBoardProperty(r.Context(), id, in.Name, in.Options, in.Position)
if err != nil {
mapError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}delete method · go · L66-L73 (8 LOC)backend/internal/http/property_handler.go
func (h *propertyHandler) delete(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "propId")
if err := h.svc.DeleteBoardProperty(r.Context(), id); err != nil {
mapError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}Citation: Repobility (2026). State of AI-Generated Code. https://repobility.com/research/
NewRouter function · go · L16-L92 (77 LOC)backend/internal/http/server.go
func NewRouter(svc *service.Service, static fs.FS) http.Handler {
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer)
r.Use(middleware.Logger)
r.Use(ValidateUTF8)
ih := &issueHandler{svc: svc}
bh := &boardHandler{svc: svc}
ch := &calendarHandler{svc: svc}
ph := &propertyHandler{svc: svc}
wh := &webhookHandler{svc: svc}
cmh := &commentHandler{svc: svc}
r.Route("/api", func(api chi.Router) {
api.Get("/health", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "version": "0.0.1"})
})
api.Route("/boards", func(b chi.Router) {
b.Get("/", bh.list)
b.Post("/", bh.create)
b.Patch("/{id}", bh.patch)
b.Delete("/{id}", bh.delete)
})
// 보드 속성(커스텀 프로퍼티)
api.Route("/boards/{boardId}/properties", func(p chi.Router) {
p.Get("/", ph.list)
p.Post("/", ph.create)
p.Patch("/{propId}", ph.update)
p.Delete("/{propId}", ph.delete)
})
// 웹훅
api.Route("/boards/{boardId}/webSPAHandler function · go · L11-L27 (17 LOC)backend/internal/http/static.go
func SPAHandler(staticFS fs.FS) http.Handler {
fileServer := http.FileServer(http.FS(staticFS))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
upath := strings.TrimPrefix(r.URL.Path, "/")
if upath == "" {
upath = "index.html"
}
if _, err := fs.Stat(staticFS, upath); err != nil {
// 존재하지 않는 경로 → index.html 로 재작성 (SPA 라우팅)
r2 := r.Clone(r.Context())
r2.URL.Path = "/"
fileServer.ServeHTTP(w, r2)
return
}
fileServer.ServeHTTP(w, r)
})
}list method · go · L15-L26 (12 LOC)backend/internal/http/webhook_handler.go
func (h *webhookHandler) list(w http.ResponseWriter, r *http.Request) {
boardID := chi.URLParam(r, "boardId")
out, err := h.svc.ListWebhooks(r.Context(), boardID)
if err != nil {
mapError(w, err)
return
}
if out == nil {
out = []domain.Webhook{}
}
writeJSON(w, http.StatusOK, out)
}create method · go · L28-L45 (18 LOC)backend/internal/http/webhook_handler.go
func (h *webhookHandler) create(w http.ResponseWriter, r *http.Request) {
boardID := chi.URLParam(r, "boardId")
var in struct {
Name string `json:"name"`
URL string `json:"url"`
Events []domain.WebhookEvent `json:"events"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
out, err := h.svc.CreateWebhook(r.Context(), boardID, in.Name, in.URL, in.Events)
if err != nil {
mapError(w, err)
return
}
writeJSON(w, http.StatusCreated, out)
}update method · go · L47-L65 (19 LOC)backend/internal/http/webhook_handler.go
func (h *webhookHandler) update(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "whId")
var in struct {
Name *string `json:"name,omitempty"`
URL *string `json:"url,omitempty"`
Events *[]domain.WebhookEvent `json:"events,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeErr(w, http.StatusBadRequest, "invalid_json", err.Error())
return
}
out, err := h.svc.UpdateWebhook(r.Context(), id, in.Name, in.URL, in.Events, in.Enabled)
if err != nil {
mapError(w, err)
return
}
writeJSON(w, http.StatusOK, out)
}delete method · go · L67-L74 (8 LOC)backend/internal/http/webhook_handler.go
func (h *webhookHandler) delete(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "whId")
if err := h.svc.DeleteWebhook(r.Context(), id); err != nil {
mapError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}New function · go · L23-L28 (6 LOC)backend/internal/service/service.go
func New(repo storage.Repo, now func() time.Time) *Service {
if now == nil {
now = func() time.Time { return time.Now().UTC() }
}
return &Service{repo: repo, now: now}
}CreateIssue method · go · L48-L73 (26 LOC)backend/internal/service/service.go
func (s *Service) CreateIssue(ctx context.Context, in CreateIssueInput) (domain.Issue, error) {
if in.Title == "" {
return domain.Issue{}, ErrEmptyTitle
}
if in.BoardID == "" {
return domain.Issue{}, ErrMissingBoard
}
now := s.now()
iss := domain.Issue{
ID: uuid.NewString(),
BoardID: in.BoardID,
ParentID: in.ParentID,
Title: in.Title,
Body: in.Body,
Status: domain.StatusPending,
Properties: map[string]any{},
CreatedAt: now,
UpdatedAt: now,
}
created, err := s.repo.Issues().Create(ctx, iss)
if err != nil {
return domain.Issue{}, err
}
s.DispatchWebhook(created.BoardID, domain.EventIssueCreated, &created)
return created, nil
}All rows above produced by Repobility · https://repobility.com
UpdateIssue method · go · L86-L132 (47 LOC)backend/internal/service/service.go
func (s *Service) UpdateIssue(ctx context.Context, id string, in UpdateIssueInput) (domain.Issue, error) {
// 상태 전이 검증 먼저 (DB 왕복 전)
var patchStatus *domain.Status
if in.Status != nil {
cur, err := s.repo.Issues().GetByID(ctx, id)
if err != nil {
return domain.Issue{}, err
}
if *in.Status == cur.Status {
// 같은 상태는 no-op로 허용
} else if err := domain.ValidateTransition(cur.Status, *in.Status); err != nil {
switch {
case errors.Is(err, domain.ErrDirectApproval):
return domain.Issue{}, ErrApproveViaPatch
case errors.Is(err, domain.ErrBackwardTransition):
return domain.Issue{}, ErrBackwardTransition
default:
return domain.Issue{}, ErrInvalidTransition
}
}
// QA → Done 은 Complete 메서드로 completed_at 기록
if cur.Status == domain.StatusQA && *in.Status == domain.StatusDone {
done, err := s.repo.Issues().Complete(ctx, id, s.now())
if err != nil {
return domain.Issue{}, err
}
// Title/Body/ParentID 도 추가로 변경이 필요할 수 있음
if in.Title ==ApproveIssue method · go · L134-L141 (8 LOC)backend/internal/service/service.go
func (s *Service) ApproveIssue(ctx context.Context, id string) (domain.Issue, error) {
approved, err := s.repo.Issues().Approve(ctx, id, s.now())
if err != nil {
return domain.Issue{}, err
}
s.DispatchWebhook(approved.BoardID, domain.EventIssueApproved, &approved)
return approved, nil
}CompleteIssue method · go · L143-L145 (3 LOC)backend/internal/service/service.go
func (s *Service) CompleteIssue(ctx context.Context, id string) (domain.Issue, error) {
return s.repo.Issues().Complete(ctx, id, s.now())
}page 1 / 6next ›