← back to kjm99d__MonkeyPlanner

Function bodies 254 total

All specs Real LLM only Function bodies
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, &params); 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,
		}, ni
Citation: 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}/web
SPAHandler 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 ›