Skip to content

Commit f356ae1

Browse files
committed
feat: アンケートAPIの実装とCosmos DB統合
1 parent 0572f3f commit f356ae1

File tree

8 files changed

+613
-7
lines changed

8 files changed

+613
-7
lines changed

Functions/Survey/Survey/Function1.cs

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,129 @@
22
using Microsoft.AspNetCore.Mvc;
33
using Microsoft.Azure.Functions.Worker;
44
using Microsoft.Extensions.Logging;
5+
using Survey.Models;
6+
using Survey.Services;
7+
using System.Text.Json;
58

69
namespace Survey;
710

8-
public class Function1
11+
public class SurveyFunctions
912
{
10-
private readonly ILogger<Function1> _logger;
13+
private readonly ILogger<SurveyFunctions> _logger;
14+
private readonly ICosmosDbService _cosmosDbService;
1115

12-
public Function1(ILogger<Function1> logger)
16+
public SurveyFunctions(ILogger<SurveyFunctions> logger, ICosmosDbService cosmosDbService)
1317
{
1418
_logger = logger;
19+
_cosmosDbService = cosmosDbService;
1520
}
1621

17-
[Function("Function1")]
18-
public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
22+
[Function("CreateSurvey")]
23+
public async Task<IActionResult> CreateSurvey(
24+
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "surveys")] HttpRequest req)
1925
{
20-
_logger.LogInformation("C# HTTP trigger function processed a request.");
21-
return new OkObjectResult("Welcome to Azure Functions!");
26+
_logger.LogInformation("アンケート登録APIが呼ばれました");
27+
28+
try
29+
{
30+
// リクエストボディを読み取り
31+
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
32+
33+
if (string.IsNullOrWhiteSpace(requestBody))
34+
{
35+
_logger.LogWarning("リクエストボディが空です");
36+
return new BadRequestObjectResult(
37+
ApiResponse<object>.ErrorResponse("リクエストボディが必要です", "EMPTY_BODY"));
38+
}
39+
40+
// JSONデシリアライズ
41+
var survey = JsonSerializer.Deserialize<Models.Survey>(requestBody, new JsonSerializerOptions
42+
{
43+
PropertyNameCaseInsensitive = true
44+
});
45+
46+
if (survey == null)
47+
{
48+
_logger.LogWarning("JSONの解析に失敗しました");
49+
return new BadRequestObjectResult(
50+
ApiResponse<object>.ErrorResponse("無効なJSONフォーマットです", "INVALID_JSON"));
51+
}
52+
53+
// バリデーション
54+
if (!survey.IsValid(out var errors))
55+
{
56+
_logger.LogWarning("バリデーションエラー: {Errors}", string.Join(", ", errors));
57+
return new BadRequestObjectResult(
58+
ApiResponse<object>.ErrorResponse(string.Join(", ", errors), "VALIDATION_ERROR"));
59+
}
60+
61+
// 新しいIDと日時を設定
62+
survey.Id = Guid.NewGuid().ToString();
63+
survey.EventDate = "2025-07-09"; // イベント日付
64+
survey.CreatedAt = DateTime.UtcNow;
65+
survey.UpdatedAt = DateTime.UtcNow;
66+
67+
// Cosmos DBに保存
68+
var surveyId = await _cosmosDbService.CreateSurveyAsync(survey);
69+
70+
_logger.LogInformation("アンケートが正常に登録されました: {SurveyId}", surveyId);
71+
72+
var response = ApiResponse<object>.SuccessResponse(
73+
"アンケートの登録が完了しました",
74+
null,
75+
surveyId);
76+
77+
return new OkObjectResult(response);
78+
}
79+
catch (JsonException ex)
80+
{
81+
_logger.LogError(ex, "JSONの解析中にエラーが発生しました");
82+
return new BadRequestObjectResult(
83+
ApiResponse<object>.ErrorResponse("無効なJSONフォーマットです", "JSON_PARSE_ERROR"));
84+
}
85+
catch (InvalidOperationException ex)
86+
{
87+
_logger.LogError(ex, "無効な操作が実行されました");
88+
return new UnprocessableEntityObjectResult(
89+
ApiResponse<object>.ErrorResponse(ex.Message, "INVALID_OPERATION"));
90+
}
91+
catch (Exception ex)
92+
{
93+
_logger.LogError(ex, "アンケート登録中に予期しないエラーが発生しました");
94+
return new ObjectResult(
95+
ApiResponse<object>.ErrorResponse("内部サーバーエラーが発生しました", "INTERNAL_ERROR"))
96+
{
97+
StatusCode = 500
98+
};
99+
}
100+
}
101+
102+
[Function("GetSurveyResults")]
103+
public async Task<IActionResult> GetSurveyResults(
104+
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "surveys/results")] HttpRequest req)
105+
{
106+
_logger.LogInformation("集計結果取得APIが呼ばれました");
107+
108+
try
109+
{
110+
var results = await _cosmosDbService.GetSurveyResultsAsync();
111+
112+
_logger.LogInformation("集計結果を正常に取得しました");
113+
114+
var response = ApiResponse<SurveyData>.SuccessResponse(
115+
"集計結果を取得しました",
116+
results);
117+
118+
return new OkObjectResult(response);
119+
}
120+
catch (Exception ex)
121+
{
122+
_logger.LogError(ex, "集計結果取得中に予期しないエラーが発生しました");
123+
return new ObjectResult(
124+
ApiResponse<object>.ErrorResponse("内部サーバーエラーが発生しました", "INTERNAL_ERROR"))
125+
{
126+
StatusCode = 500
127+
};
128+
}
22129
}
23130
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Text.Json.Serialization;
2+
using Newtonsoft.Json;
3+
4+
namespace Survey.Models;
5+
6+
public class ApiResponse<T>
7+
{
8+
[JsonPropertyName("success")]
9+
[JsonProperty("success")]
10+
public bool Success { get; set; }
11+
12+
[JsonPropertyName("message")]
13+
[JsonProperty("message")]
14+
public string? Message { get; set; }
15+
16+
[JsonPropertyName("error")]
17+
[JsonProperty("error")]
18+
public string? Error { get; set; }
19+
20+
[JsonPropertyName("code")]
21+
[JsonProperty("code")]
22+
public string? Code { get; set; }
23+
24+
[JsonPropertyName("surveyId")]
25+
[JsonProperty("surveyId")]
26+
public string? SurveyId { get; set; }
27+
28+
[JsonPropertyName("data")]
29+
[JsonProperty("data")]
30+
public T? Data { get; set; }
31+
32+
public static ApiResponse<T> SuccessResponse(string message, T? data = default, string? surveyId = null)
33+
{
34+
return new ApiResponse<T>
35+
{
36+
Success = true,
37+
Message = message,
38+
Data = data,
39+
SurveyId = surveyId
40+
};
41+
}
42+
43+
public static ApiResponse<T> ErrorResponse(string error, string? code = null)
44+
{
45+
return new ApiResponse<T>
46+
{
47+
Success = false,
48+
Error = error,
49+
Code = code
50+
};
51+
}
52+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.Text.Json.Serialization;
3+
using Newtonsoft.Json;
4+
5+
namespace Survey.Models;
6+
7+
public class Survey
8+
{
9+
[JsonPropertyName("id")]
10+
[JsonProperty("id")]
11+
public string Id { get; set; } = Guid.NewGuid().ToString();
12+
13+
[JsonPropertyName("eventDate")]
14+
[JsonProperty("eventDate")]
15+
public string EventDate { get; set; } = "2025-07-09"; // パーティションキー
16+
17+
[JsonPropertyName("communityAffiliation")]
18+
[JsonProperty("communityAffiliation")]
19+
[Required]
20+
public string[] CommunityAffiliation { get; set; } = Array.Empty<string>();
21+
22+
[JsonPropertyName("jobRole")]
23+
[JsonProperty("jobRole")]
24+
[Required]
25+
[MinLength(1, ErrorMessage = "職種は1つ以上選択してください")]
26+
public string[] JobRole { get; set; } = Array.Empty<string>();
27+
28+
[JsonPropertyName("jobRoleOther")]
29+
[JsonProperty("jobRoleOther")]
30+
[MaxLength(100, ErrorMessage = "その他の職種は100文字以内で入力してください")]
31+
public string? JobRoleOther { get; set; }
32+
33+
[JsonPropertyName("eventRating")]
34+
[JsonProperty("eventRating")]
35+
[Required]
36+
[Range(1, 5, ErrorMessage = "イベント評価は1-5の範囲で入力してください")]
37+
public int EventRating { get; set; }
38+
39+
[JsonPropertyName("feedback")]
40+
[JsonProperty("feedback")]
41+
[MaxLength(1000, ErrorMessage = "フィードバックは1000文字以内で入力してください")]
42+
public string? Feedback { get; set; }
43+
44+
[JsonPropertyName("createdAt")]
45+
[JsonProperty("createdAt")]
46+
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
47+
48+
[JsonPropertyName("updatedAt")]
49+
[JsonProperty("updatedAt")]
50+
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
51+
52+
// バリデーションメソッド
53+
public bool IsValid(out List<string> errors)
54+
{
55+
errors = new List<string>();
56+
57+
if (CommunityAffiliation == null)
58+
{
59+
errors.Add("コミュニティ所属は必須です");
60+
}
61+
62+
if (JobRole == null || JobRole.Length == 0)
63+
{
64+
errors.Add("職種は1つ以上選択してください");
65+
}
66+
67+
if (JobRole?.Contains("その他") == true && string.IsNullOrWhiteSpace(JobRoleOther))
68+
{
69+
errors.Add("「その他」を選択した場合は具体的な職種を入力してください");
70+
}
71+
72+
if (EventRating < 1 || EventRating > 5)
73+
{
74+
errors.Add("イベント評価は1-5の範囲で入力してください");
75+
}
76+
77+
return errors.Count == 0;
78+
}
79+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.Text.Json.Serialization;
2+
using Newtonsoft.Json;
3+
4+
namespace Survey.Models;
5+
6+
public class SurveyResult
7+
{
8+
[JsonPropertyName("success")]
9+
[JsonProperty("success")]
10+
public bool Success { get; set; }
11+
12+
[JsonPropertyName("data")]
13+
[JsonProperty("data")]
14+
public SurveyData? Data { get; set; }
15+
}
16+
17+
public class SurveyData
18+
{
19+
[JsonPropertyName("totalResponses")]
20+
[JsonProperty("totalResponses")]
21+
public int TotalResponses { get; set; }
22+
23+
[JsonPropertyName("communityAffiliation")]
24+
[JsonProperty("communityAffiliation")]
25+
public Dictionary<string, int> CommunityAffiliation { get; set; } = new();
26+
27+
[JsonPropertyName("jobRole")]
28+
[JsonProperty("jobRole")]
29+
public Dictionary<string, int> JobRole { get; set; } = new();
30+
31+
[JsonPropertyName("eventRating")]
32+
[JsonProperty("eventRating")]
33+
public EventRatingData EventRating { get; set; } = new();
34+
35+
[JsonPropertyName("feedback")]
36+
[JsonProperty("feedback")]
37+
public List<FeedbackItem> Feedback { get; set; } = new();
38+
}
39+
40+
public class EventRatingData
41+
{
42+
[JsonPropertyName("average")]
43+
[JsonProperty("average")]
44+
public double Average { get; set; }
45+
46+
[JsonPropertyName("distribution")]
47+
[JsonProperty("distribution")]
48+
public Dictionary<string, int> Distribution { get; set; } = new();
49+
}
50+
51+
public class FeedbackItem
52+
{
53+
[JsonPropertyName("id")]
54+
[JsonProperty("id")]
55+
public string Id { get; set; } = string.Empty;
56+
57+
[JsonPropertyName("feedback")]
58+
[JsonProperty("feedback")]
59+
public string Feedback { get; set; } = string.Empty;
60+
61+
[JsonPropertyName("timestamp")]
62+
[JsonProperty("timestamp")]
63+
public DateTime Timestamp { get; set; }
64+
}

Functions/Survey/Survey/Program.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using Microsoft.Azure.Functions.Worker.Builder;
33
using Microsoft.Extensions.DependencyInjection;
44
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Azure.Cosmos;
6+
using Survey.Services;
57

68
var builder = FunctionsApplication.CreateBuilder(args);
79

@@ -11,4 +13,28 @@
1113
.AddApplicationInsightsTelemetryWorkerService()
1214
.ConfigureFunctionsApplicationInsights();
1315

16+
// Cosmos DB クライアントの設定
17+
builder.Services.AddSingleton<CosmosClient>(serviceProvider =>
18+
{
19+
var connectionString = Environment.GetEnvironmentVariable("CosmosDbConnectionString");
20+
if (string.IsNullOrEmpty(connectionString))
21+
{
22+
throw new InvalidOperationException("CosmosDbConnectionString environment variable is not set");
23+
}
24+
25+
var cosmosClientOptions = new CosmosClientOptions
26+
{
27+
RequestTimeout = TimeSpan.FromSeconds(30),
28+
OpenTcpConnectionTimeout = TimeSpan.FromSeconds(30),
29+
GatewayModeMaxConnectionLimit = 50,
30+
MaxRetryAttemptsOnRateLimitedRequests = 3,
31+
MaxRetryWaitTimeOnRateLimitedRequests = TimeSpan.FromSeconds(30)
32+
};
33+
34+
return new CosmosClient(connectionString, cosmosClientOptions);
35+
});
36+
37+
// Cosmos DB サービスの登録
38+
builder.Services.AddScoped<ICosmosDbService, CosmosDbService>();
39+
1440
builder.Build().Run();

0 commit comments

Comments
 (0)